├── .gitignore
├── LICENSE
├── README.md
├── assets
├── banner-styles.jpg
├── github-hero-banner.jpg
└── react-cookie-manager.gif
├── package-lock.json
├── package.json
├── playground-next
├── next-env.d.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.js
├── src
│ ├── app
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── ssr-example
│ │ │ └── page.tsx
│ ├── components
│ │ └── Providers.tsx
│ └── i18n.ts
├── tailwind.config.ts
└── tsconfig.json
├── playground
├── .gitignore
├── README.md
├── eslint.config.js
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── src
│ ├── App.css
│ ├── App.tsx
│ ├── assets
│ │ └── react.svg
│ ├── globals.css
│ ├── index.css
│ ├── main.tsx
│ └── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
├── postcss.config.js
├── scripts
└── postinstall.js
├── src
├── components
│ ├── CookieConsenter.tsx
│ ├── FloatingCookieButton.tsx
│ └── ManageConsent.tsx
├── context
│ └── CookieConsentContext.tsx
├── index.ts
├── styles
│ └── tailwind.css
├── types
│ └── types.ts
└── utils
│ ├── cn.ts
│ ├── cookie-blocking
│ ├── content-blocker.tsx
│ ├── index.ts
│ └── request-blocker.ts
│ ├── cookie-utils.ts
│ ├── session-utils.ts
│ ├── timeZoneMap.ts
│ ├── tracker-utils.ts
│ ├── trackers.ts
│ └── translations.ts
├── tailwind.config.js
├── tsconfig.json
└── vite.config.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | # Environment files
27 | .env
28 | .env.local
29 | .env.development
30 | .env.test
31 | .env.production
32 |
33 | # Next.js
34 | .next
35 |
36 | # Build artifacts
37 | *.zip
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Hypership
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🍪 React Cookie Manager
2 |
3 | A powerful, customizable React component for cookie consent management with built-in tracking prevention. This component provides a modern, user-friendly way to obtain and manage cookie consent from your website visitors.
4 |
5 | [](https://cookiekit.io)
6 |
7 | 
8 |
9 | ## Quick Start
10 |
11 | Get up and running quickly with React Cookie Manager:
12 |
13 | ```bash
14 | npm install react-cookie-manager
15 | # or
16 | yarn add react-cookie-manager
17 | ```
18 |
19 | ```jsx
20 | import { CookieManager } from "react-cookie-manager";
21 | import "react-cookie-manager/style.css";
22 |
23 | createRoot(document.getElementById("root")).render(
24 |
25 |
26 |
27 |
28 |
29 | );
30 | ```
31 |
32 | The CookieManager component needs to wrap your entire application to properly manage cookie consent across all components and pages.
33 |
34 | ## Contents
35 |
36 | - [Quick Start](#quick-start)
37 | - [Features](#features)
38 | - [Try it out](#-try-it-out)
39 | - [CookieKit Integration](#cookiekit-integration)
40 | - [Automatically Disable Tracking](#automatically-disable-tracking)
41 | - [Installation](#installation)
42 | - [Importing Styles](#importing-styles)
43 | - [Basic Usage](#basic-usage)
44 | - [Next.js Usage](#nextjs-usage)
45 | - [Full Usage](#full-usage)
46 | - [Advanced Usage with Hook](#advanced-usage-with-hook)
47 | - [Props](#props)
48 | - [CSS Customization](#css-customization)
49 | - [Available classNames](#available-classnames)
50 | - [CSS Framework Compatibility](#css-framework-compatibility)
51 | - [Element Groups](#element-groups)
52 | - [Cookie Categories](#cookie-categories)
53 | - [Hook API](#hook-api)
54 | - [i18next support](#i18next-support)
55 | - [Translation Options](#translation-options)
56 | - [Contributing](#contributing)
57 | - [License](#license)
58 |
59 | ## Features
60 |
61 | - 🌐 Multiple display types (banner, popup, modal)
62 | - 🛡️ Automatic tracking prevention (Google Analytics, etc.)
63 | - 🎬 Smart iframe blocking for embedded content (YouTube, Vimeo, etc.)
64 | - 🎯 Granular cookie category controls (Analytics, Social, Advertising)
65 | - 🎨 Light and dark theme support
66 | - 📱 Responsive design
67 | - 🔧 Highly customizable UI
68 | - 💾 Persistent consent storage
69 | - 🔒 Privacy-first approach
70 | - 🇪🇺 GDPR compliance with CookieKit.io integration
71 | - 🍪 Floating cookie button for easy access
72 |
73 | ## CookieKit Integration
74 |
75 | Take your GDPR compliance to the next level with [CookieKit.io](https://cookiekit.io) integration!
76 |
77 | ### Features
78 |
79 | - 📊 Real-time consent analytics dashboard
80 | - 🔄 Automatic consent proof storage
81 | - 📈 Advanced user segmentation
82 | - 🆓 Completely free to use!
83 |
84 | ### Usage with CookieKit
85 |
86 | ```jsx
87 | import { CookieManager } from "react-cookie-manager";
88 |
89 | function App() {
90 | return (
91 |
98 |
99 |
100 | );
101 | }
102 | ```
103 |
104 | When `cookieKitId` is provided, React Cookie Manager will automatically:
105 |
106 | - Generate and track unique session IDs
107 | - Send consent events to CookieKit.io
108 | - Store consent proofs for GDPR compliance
109 | - Provide analytics data in your CookieKit dashboard
110 |
111 | Visit [cookiekit.io](https://cookiekit.io) to get started for free!
112 |
113 | ## 🎮 Try it out!
114 |
115 | ### [🔗 Live Demo](https://cookiekit.io/playground)
116 |
117 | See React Cookie Manager in action and explore all its features in our interactive demo.
118 |
119 | ## Automatically Disable Tracking
120 |
121 | Unlike other cookie consent managers and React components, this component automatically disables tracking for Google Analytics, Facebook Pixel, and other tracking services. This is done by blocking the tracking scripts from loading. Therefore, you don't need to manually disable tracking, saving you hours of work.
122 |
123 | ### Embedded Content Blocking
124 |
125 | React Cookie Manager automatically blocks embedded iframes that would otherwise load cookies without consent, such as:
126 |
127 | - YouTube videos
128 | - Vimeo videos
129 | - Google Maps
130 | - Social media embeds (Twitter, Instagram, etc.)
131 | - Third-party widgets and tools
132 |
133 | When a user hasn't consented to the required cookies, these embeds are replaced with user-friendly placeholders that:
134 |
135 | - Explain why the content is blocked
136 | - Provide a button to manage cookie settings
137 | - Inform users to refresh the page after accepting cookies
138 | - Maintain the same dimensions as the original content
139 |
140 | This ensures your site remains GDPR-compliant while providing a seamless user experience.
141 |
142 | ## Installation
143 |
144 | ```bash
145 | npm install react-cookie-manager
146 | # or
147 | yarn add react-cookie-manager
148 | ```
149 |
150 | ## Importing Styles
151 |
152 | The component requires its CSS file to be imported in your application. Add the following import to your app's entry point (e.g., `App.tsx` or `index.tsx`):
153 |
154 | ```javascript
155 | import "react-cookie-manager/style.css";
156 | ```
157 |
158 | 
159 |
160 | ## Basic Usage
161 |
162 | ```jsx
163 | import { CookieManager } from "react-cookie-manager";
164 | import "react-cookie-manager/style.css";
165 |
166 | function App() {
167 | return (
168 |
174 | console.log("Cookie preferences:", preferences)
175 | }
176 | >
177 |
178 |
179 | );
180 | }
181 | ```
182 |
183 | ## Next.js Usage
184 |
185 | For Next.js applications, you'll need to use dynamic imports to prevent SSR of the cookie manager:
186 |
187 | ```tsx
188 | "use client";
189 |
190 | import dynamic from "next/dynamic";
191 |
192 | const CookieManager = dynamic(
193 | () => import("react-cookie-manager").then((mod) => mod.CookieManager),
194 | { ssr: false, loading: () => null }
195 | );
196 |
197 | // In your Providers component or layout
198 | export function Providers({ children }: { children: React.ReactNode }) {
199 | return (
200 |
209 | {children}
210 |
211 | );
212 | }
213 |
214 | // In your page component
215 | import { useCookieConsent } from "react-cookie-manager";
216 |
217 | export default function Home() {
218 | const { showConsentBanner, detailedConsent } = useCookieConsent();
219 |
220 | return (
221 |
222 |
223 | {detailedConsent && (
224 |
225 | Analytics:{" "}
226 | {detailedConsent.Analytics.consented ? "Enabled" : "Disabled"}
227 | Social: {detailedConsent.Social.consented ? "Enabled" : "Disabled"}
228 | Advertising:{" "}
229 | {detailedConsent.Advertising.consented ? "Enabled" : "Disabled"}
230 |
231 | )}
232 |
233 | );
234 | }
235 | ```
236 |
237 | ## Full Usage
238 |
239 | ```jsx
240 | import { CookieManager } from "react-cookie-manager";
241 | import "react-cookie-manager/style.css";
242 |
243 | function App() {
244 | return (
245 | {
261 | if (preferences) {
262 | console.log("Cookie preferences updated:", preferences);
263 | }
264 | }}
265 | onAccept={() => {
266 | console.log("User accepted all cookies");
267 | // Analytics tracking can be initialized here
268 | }}
269 | onDecline={() => {
270 | console.log("User declined all cookies");
271 | // Handle declined state if needed
272 | }}
273 | >
274 |
275 |
276 | );
277 | }
278 | ```
279 |
280 | ## Advanced Usage with Hook
281 |
282 | ```jsx
283 | import { CookieManager, useCookieConsent } from "react-cookie-manager";
284 |
285 | function CookieSettings() {
286 | const { showConsentBanner, detailedConsent } = useCookieConsent();
287 |
288 | return (
289 |
290 |
291 | {detailedConsent && (
292 |
293 | Analytics:{" "}
294 | {detailedConsent.Analytics.consented ? "Enabled" : "Disabled"}
295 | Social: {detailedConsent.Social.consented ? "Enabled" : "Disabled"}
296 | Advertising:{" "}
297 | {detailedConsent.Advertising.consented ? "Enabled" : "Disabled"}
298 |
299 | )}
300 |
301 | );
302 | }
303 | ```
304 |
305 | ## Floating Cookie Button
306 |
307 | The floating cookie button provides a persistent, accessible way for users to manage their cookie preferences after they've made their initial choice. It appears as a small, animated cookie icon in the bottom-left corner of the screen.
308 |
309 | ### Enabling the Floating Button
310 |
311 | ```jsx
312 |
317 |
318 |
319 | ```
320 |
321 | ### Features
322 |
323 | - 🎯 Automatically appears after initial consent
324 | - 🎨 Matches your theme (light/dark mode)
325 | - 🔄 Smooth animations and hover effects
326 | - ❌ Dismissible with a close button
327 | - 📱 Responsive and mobile-friendly
328 | - 🎛️ Easy access to cookie preferences
329 |
330 | ### Behavior
331 |
332 | 1. The button appears after users make their initial cookie choice
333 | 2. Hovering reveals a close button to dismiss the floating button
334 | 3. Clicking opens the cookie preferences modal
335 | 4. The button remains hidden until page refresh after being closed
336 | 5. Maintains position during scroll
337 |
338 | ### Customization
339 |
340 | The floating button automatically adapts to your chosen theme:
341 |
342 | ```jsx
343 | // Light theme (default)
344 |
348 |
349 |
350 |
351 | // Dark theme
352 |
356 |
357 |
358 | ```
359 |
360 | The button inherits your color scheme:
361 |
362 | - Light theme: White background with gray text
363 | - Dark theme: Black background with light gray text
364 |
365 | ### Accessibility
366 |
367 | The floating button is fully accessible:
368 |
369 | - Proper ARIA labels
370 | - Keyboard navigation support
371 | - Focus management
372 | - High contrast ratios
373 | - Screen reader friendly
374 |
375 | ## Props
376 |
377 | | Prop | Type | Default | Description |
378 | | -------------------------- | ---------------------------------------- | ---------------- | ----------------------------------------- |
379 | | `children` | React.ReactNode | - | Your app components |
380 | | `translations` | TranslationObject \| TranslationFunction | - | Translation object or i18n TFunction |
381 | | `translationI18NextPrefix` | string | - | i18next key prefix, e.g. "cookies." |
382 | | `showManageButton` | boolean | false | Whether to show the manage cookies button |
383 | | `enableFloatingButton` | boolean | false | Enable floating cookie button |
384 | | `privacyPolicyUrl` | string | - | URL for the privacy policy |
385 | | `cookieKey` | string | 'cookie-consent' | Name of the cookie to store consent |
386 | | `cookieExpiration` | number | 365 | Days until cookie expires |
387 | | `displayType` | 'banner' \| 'popup' \| 'modal' | 'banner' | How the consent UI is displayed |
388 | | `position` | 'top' \| 'bottom' | 'bottom' | Position of the banner |
389 | | `theme` | 'light' \| 'dark' | 'light' | Color theme |
390 | | `disableAutomaticBlocking` | boolean | false | Disable automatic tracking prevention |
391 | | `blockedDomains` | string[] | [] | Additional domains to block |
392 | | `cookieKitId` | string | undefined | Your CookieKit.io integration ID |
393 | | `onManage` | (preferences?: CookieCategories) => void | - | Callback when preferences are updated |
394 | | `onAccept` | () => void | - | Callback when all cookies are accepted |
395 | | `onDecline` | () => void | - | Callback when all cookies are declined |
396 | | `classNames` | CookieConsenterClassNames | - | Custom class names for styling |
397 |
398 | ## CSS Customization
399 |
400 | React Cookie Manager provides extensive styling customization through the `classNames` prop. You can override the default styling for each element of the cookie consent UI.
401 |
402 | ### Available classNames
403 |
404 | ```tsx
405 |
458 | {children}
459 |
460 | ```
461 |
462 | ### CSS Framework Compatibility
463 |
464 | The `classNames` prop is compatible with any CSS framework. Here are some examples:
465 |
466 | #### Tailwind CSS
467 |
468 | ```tsx
469 |
478 | {children}
479 |
480 | ```
481 |
482 | #### Bootstrap
483 |
484 | ```tsx
485 |
496 | {children}
497 |
498 | ```
499 |
500 | ### Element Groups
501 |
502 | The classNames are organized by component type:
503 |
504 | #### Button Elements
505 |
506 | - `acceptButton`: Style for the Accept/Allow cookies button
507 | - `declineButton`: Style for the Decline/Reject cookies button
508 | - `manageButton`: Style for the Manage Cookies button
509 | - `manageCancelButton`: Style for the Cancel button in the manage preferences view
510 | - `manageSaveButton`: Style for the Save Preferences button
511 |
512 | #### Container Elements
513 |
514 | - `bannerContainer`: Main container for the banner-style consent UI
515 | - `popupContainer`: Main container for the popup-style consent UI
516 | - `modalContainer`: Main container for the modal-style consent UI
517 | - `manageCookieContainer`: Container for the manage preferences UI
518 |
519 | #### Content Elements
520 |
521 | - `bannerContent`, `popupContent`, `modalContent`: Content containers for each display type
522 | - `bannerTitle`, `popupTitle`, `modalTitle`: Title elements for each display type
523 | - `bannerMessage`, `popupMessage`, `modalMessage`: Message elements for each display type
524 |
525 | #### Manage Cookie UI Elements
526 |
527 | - `manageCookieTitle`: Title for the manage cookie preferences UI
528 | - `manageCookieMessage`: Description text in the manage preferences UI
529 | - `manageCookieCategory`: Container for each cookie category
530 | - `manageCookieCategoryTitle`: Title for each cookie category
531 | - `manageCookieCategorySubtitle`: Description for each cookie category
532 | - `manageCookieStatusText`: Status text showing consent status and date
533 | - `manageCookieToggle`: Toggle switch for cookie categories
534 | - `manageCookieToggleChecked`: Style applied to the toggle when checked
535 |
536 | #### Other Elements
537 |
538 | - `privacyPolicyLink`: Style for the privacy policy link
539 | - `floatingButton`: Style for the floating cookie button
540 | - `floatingButtonCloseButton`: Style for the close button on the floating cookie button
541 | - `poweredByLink`: Style for the "Powered by CookieKit" link
542 |
543 | ## Cookie Categories
544 |
545 | The component manages three categories of cookies:
546 |
547 | ```typescript
548 | interface CookieCategories {
549 | Analytics: boolean;
550 | Social: boolean;
551 | Advertising: boolean;
552 | }
553 | ```
554 |
555 | ## Hook API
556 |
557 | The `useCookieConsent` hook provides the following:
558 |
559 | ```typescript
560 | interface CookieConsentHook {
561 | hasConsent: boolean | null;
562 | isDeclined: boolean;
563 | detailedConsent: DetailedCookieConsent | null;
564 | showConsentBanner: () => void;
565 | acceptCookies: () => void;
566 | declineCookies: () => void;
567 | updateDetailedConsent: (preferences: CookieCategories) => void;
568 | }
569 | ```
570 |
571 | ## Event Callbacks
572 |
573 | The CookieManager component provides callback props that allow you to respond to user interactions with the consent UI:
574 |
575 | | Callback | Triggered when | Parameters |
576 | | ----------- | ------------------------------------ | -------------------------------- |
577 | | `onAccept` | User accepts all cookies | None |
578 | | `onDecline` | User declines all cookies | None |
579 | | `onManage` | User saves custom cookie preferences | `preferences?: CookieCategories` |
580 |
581 | ### Usage Example
582 |
583 | ```jsx
584 | {
586 | console.log("All cookies accepted");
587 | // Initialize analytics tools
588 | window.gtag?.("consent", "update", { analytics_storage: "granted" });
589 | }}
590 | onDecline={() => {
591 | console.log("All cookies declined");
592 | // Ensure tracking is disabled
593 | window.gtag?.("consent", "update", { analytics_storage: "denied" });
594 | }}
595 | onManage={(preferences) => {
596 | console.log("Custom preferences saved:", preferences);
597 | // Handle granular consent
598 | if (preferences?.Analytics) {
599 | // Enable analytics
600 | }
601 | if (preferences?.Advertising) {
602 | // Enable ad personalization
603 | }
604 | }}
605 | >
606 | {children}
607 |
608 | ```
609 |
610 | ### Common Use Cases
611 |
612 | - **Analytics Initialization**: Only initialize tracking tools after receiving explicit consent
613 | - **Ad Personalization**: Enable or disable personalized advertising based on user preferences
614 | - **Social Media Integration**: Load social widgets only when Social cookies are accepted
615 | - **Consent Logging**: Record user consent choices for compliance purposes
616 | - **UI Updates**: Update the UI based on user consent status (e.g., showing alternative content)
617 |
618 | ## i18next support
619 |
620 | ```typescript
621 | import { default as i18next } from "i18next";
622 |
623 | function App() {
624 | return (
625 |
630 | )
631 | }
632 | ```
633 |
634 | ```json
635 | // en.json
636 | {
637 | "cookies": {
638 | "title": "Would You Like A Cookie? 🍪",
639 | "message": "We value your privacy. Choose which cookies you want to allow. Essential cookies are always enabled as they are necessary for the website to function properly.",
640 | "buttonText": "Accept All",
641 | "declineButtonText": "Decline All",
642 | "manageButtonText": "Manage Cookies",
643 | "privacyPolicyText": "Privacy Policy"
644 | }
645 | //...
646 | }
647 | ```
648 |
649 | ## Translation Options
650 |
651 | All available translation keys and their default values:
652 |
653 | ```typescript
654 | {
655 | // Main consent banner/popup/modal
656 | title: "", // Optional title
657 | message: "This website uses cookies to enhance your experience.",
658 | buttonText: "Accept",
659 | declineButtonText: "Decline",
660 | manageButtonText: "Manage Cookies",
661 | privacyPolicyText: "Privacy Policy",
662 |
663 | // Manage consent modal
664 | manageTitle: "Cookie Preferences",
665 | manageMessage: "Manage your cookie preferences below. Essential cookies are always enabled as they are necessary for the website to function properly.",
666 |
667 | // Essential cookies section
668 | manageEssentialTitle: "Essential",
669 | manageEssentialSubtitle: "Required for the website to function properly",
670 | manageEssentialStatus: "Status: Always enabled",
671 | manageEssentialStatusButtonText: "Always On",
672 |
673 | // Analytics cookies section
674 | manageAnalyticsTitle: "Analytics",
675 | manageAnalyticsSubtitle: "Help us understand how visitors interact with our website",
676 |
677 | // Social cookies section
678 | manageSocialTitle: "Social",
679 | manageSocialSubtitle: "Enable social media features and sharing",
680 |
681 | // Advertising cookies section
682 | manageAdvertTitle: "Advertising",
683 | manageAdvertSubtitle: "Personalize advertisements and measure their performance",
684 |
685 | // Status messages
686 | manageCookiesStatus: "Status: {{status}} on {{date}}", // Supports variables
687 | manageCookiesStatusConsented: "Consented",
688 | manageCookiesStatusDeclined: "Declined",
689 |
690 | // Buttons in manage modal
691 | manageCancelButtonText: "Cancel",
692 | manageSaveButtonText: "Save Preferences"
693 | }
694 | ```
695 |
696 | You can override any of these translations by passing them in the `translations` prop:
697 |
698 | ```jsx
699 |
708 |
709 |
710 | ```
711 |
712 | ### i18next Integration
713 |
714 | When using i18next, make sure your translation files include all the keys under your chosen prefix:
715 |
716 | ```json
717 | {
718 | "cookies": {
719 | "title": "Cookie Settings 🍪",
720 | "message": "We use cookies to improve your experience.",
721 | "buttonText": "Allow All",
722 | "declineButtonText": "Decline All",
723 | "manageButtonText": "Customize",
724 | "privacyPolicyText": "Privacy Policy",
725 | "manageTitle": "Cookie Preferences",
726 | "manageMessage": "Customize your cookie preferences below...",
727 | "manageEssentialTitle": "Essential Cookies"
728 | // ... include all other translation keys
729 | }
730 | }
731 | ```
732 |
733 | Then use it with the i18next translation function:
734 |
735 | ```jsx
736 | import { useTranslation } from "react-i18next";
737 |
738 | function App() {
739 | const { t } = useTranslation();
740 |
741 | return (
742 |
743 |
744 |
745 | );
746 | }
747 | ```
748 |
749 | ## Contributing
750 |
751 | Contributions are welcome! Please feel free to submit a Pull Request.
752 |
753 | ## License
754 |
755 | MIT © Hypership
756 |
--------------------------------------------------------------------------------
/assets/banner-styles.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hypershiphq/react-cookie-manager/d93d03b4fd67cd50ce49eab33ffa0970fe0dcead/assets/banner-styles.jpg
--------------------------------------------------------------------------------
/assets/github-hero-banner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hypershiphq/react-cookie-manager/d93d03b4fd67cd50ce49eab33ffa0970fe0dcead/assets/github-hero-banner.jpg
--------------------------------------------------------------------------------
/assets/react-cookie-manager.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hypershiphq/react-cookie-manager/d93d03b4fd67cd50ce49eab33ffa0970fe0dcead/assets/react-cookie-manager.gif
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-cookie-manager",
3 | "version": "3.7.2",
4 | "description": "🍪 The ultimate React cookie consent solution. Automatically block trackers, manage consent preferences, and protect user privacy with an elegant UI. Perfect for modern web applications.",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/hypershiphq/react-cookie-manager.git"
8 | },
9 | "bugs": {
10 | "url": "https://github.com/hypershiphq/react-cookie-manager/issues"
11 | },
12 | "homepage": "https://github.com/hypershiphq/react-cookie-manager#readme",
13 | "keywords": [
14 | "react",
15 | "cookies",
16 | "cookie management",
17 | "react cookie banner",
18 | "react cookie consent",
19 | "react cookie manager",
20 | "gdpr",
21 | "cookie consent",
22 | "cookie blocking",
23 | "cookie consent manager",
24 | "cookie consent manager react",
25 | "analytics consent",
26 | "advertising consent",
27 | "social consent",
28 | "privacy",
29 | "tracking protection",
30 | "user consent",
31 | "cookie law",
32 | "eu cookie law",
33 | "ccpa"
34 | ],
35 | "main": "./dist/index.js",
36 | "module": "./dist/index.js",
37 | "types": "./dist/index.d.ts",
38 | "style": "./dist/style.css",
39 | "files": [
40 | "dist/**",
41 | "scripts/**"
42 | ],
43 | "exports": {
44 | ".": {
45 | "import": {
46 | "types": "./dist/index.d.ts",
47 | "default": "./dist/index.js"
48 | },
49 | "require": {
50 | "types": "./dist/index.d.ts",
51 | "default": "./dist/index.js"
52 | }
53 | },
54 | "./dist/*": "./dist/*",
55 | "./style.css": "./dist/style.css"
56 | },
57 | "scripts": {
58 | "build": "vite build",
59 | "postinstall": "node scripts/postinstall.js",
60 | "publish-npm": "node scripts/publish.js"
61 | },
62 | "type": "module",
63 | "author": "Hypership",
64 | "license": "MIT",
65 | "devDependencies": {
66 | "@types/node": "^20.17.16",
67 | "@types/react": "^18.0.0",
68 | "@types/react-dom": "^18.0.0",
69 | "@vitejs/plugin-react": "^4.3.4",
70 | "autoprefixer": "^10.4.17",
71 | "clsx": "^2.1.1",
72 | "cssnano": "^7.0.6",
73 | "postcss": "^8.4.35",
74 | "postcss-prefix-selector": "^2.1.0",
75 | "react": "^18.0.0",
76 | "react-dom": "^18.0.0",
77 | "tailwind-merge": "^3.1.0",
78 | "tailwindcss": "^3.4.1",
79 | "terser": "^5.37.0",
80 | "tsup": "^8.3.5",
81 | "typescript": "^5.5.2",
82 | "vite": "^6.0.3",
83 | "vite-plugin-dts": "^4.3.0",
84 | "vite-plugin-lib-inject-css": "^2.2.1"
85 | },
86 | "peerDependencies": {
87 | "react": ">=16.8.0",
88 | "react-dom": ">=16.8.0"
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/playground-next/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/playground-next/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/playground-next/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playground-next",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@types/node": "^20",
13 | "@types/react": "^18",
14 | "@types/react-dom": "^18",
15 | "autoprefixer": "^10.0.1",
16 | "i18next": "^23.8.2",
17 | "jotai": "^2.6.5",
18 | "next": "14.1.0",
19 | "postcss": "^8",
20 | "react": "^18",
21 | "react-cookie-manager": "^3.3.1",
22 | "react-dom": "^18",
23 | "react-i18next": "^14.0.5",
24 | "tailwindcss": "^3.3.0",
25 | "typescript": "^5"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/playground-next/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/playground-next/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/playground-next/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 | import { Providers } from "../components/Providers";
5 |
6 | const inter = Inter({ subsets: ["latin"] });
7 |
8 | export const metadata: Metadata = {
9 | title: "Cookie Consent Playground",
10 | description: "Test environment for cookie consent component",
11 | };
12 |
13 | export default function RootLayout({
14 | children,
15 | }: Readonly<{
16 | children: React.ReactNode;
17 | }>) {
18 | return (
19 |
20 |
21 | {children}
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/playground-next/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useCookieConsent } from "../../../dist";
4 | import Link from "next/link";
5 |
6 | export default function Home() {
7 | return (
8 |
9 | Cookie Consent Playground
10 |
14 | View SSR Example Page
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/playground-next/src/app/ssr-example/page.tsx:
--------------------------------------------------------------------------------
1 | // This file is an SSR page by default in Next.js App Router
2 | // No "use client" directive needed
3 |
4 | import { cookies } from "next/headers";
5 |
6 | // Server component - this code runs on the server
7 | export default async function SSRPage() {
8 | // This is server-side code
9 | const cookieStore = cookies();
10 | const theme = cookieStore.get("theme")?.value || "system";
11 |
12 | // Example of server-side data fetching
13 | const data = await fetchServerData();
14 |
15 | return (
16 |
17 |
Server-Side Rendered Page
18 |
19 | Current theme from cookie: {theme}
20 |
21 |
22 | Server timestamp: {data.timestamp}
23 |
24 |
25 | Random number generated on server: {data.randomNumber}
26 |
27 |
28 | );
29 | }
30 |
31 | // Server-side only function
32 | async function fetchServerData() {
33 | // This function only runs on the server
34 | return {
35 | timestamp: new Date().toISOString(),
36 | randomNumber: Math.floor(Math.random() * 100),
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/playground-next/src/components/Providers.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { I18nextProvider } from "react-i18next";
4 | import i18n from "../i18n";
5 | import dynamic from "next/dynamic";
6 | import "../../../dist/style.css";
7 |
8 | const CookieManager = dynamic(
9 | () => import("../../../dist").then((mod) => mod.CookieManager),
10 | {
11 | ssr: false,
12 | loading: () => null,
13 | }
14 | );
15 |
16 | export function Providers({ children }: { children: React.ReactNode }) {
17 | return (
18 |
19 | {
27 | console.log("accept");
28 | }}
29 | onDecline={() => {
30 | console.log("decline");
31 | }}
32 | onManage={() => {
33 | console.log("manage");
34 | }}
35 | >
36 | {children}
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/playground-next/src/i18n.ts:
--------------------------------------------------------------------------------
1 | import i18n from "i18next";
2 | import { initReactI18next } from "react-i18next";
3 |
4 | const resources = {
5 | en: {
6 | translation: {
7 | title: "Cookie Settings 🍪",
8 | message:
9 | "We use cookies to improve your experience on our website. Choose which cookies you want to allow.",
10 | buttonText: "Accept All",
11 | declineButtonText: "Decline All",
12 | manageButtonText: "Manage Cookies",
13 | privacyPolicyText: "Privacy Policy",
14 | manageTitle: "Cookie Preferences",
15 | manageMessage: "Customize your cookie preferences below",
16 | manageEssentialTitle: "Essential Cookies",
17 | manageAnalyticsTitle: "Analytics",
18 | manageSocialTitle: "Social",
19 | manageAdvertTitle: "Advertising",
20 | },
21 | },
22 | };
23 |
24 | i18n.use(initReactI18next).init({
25 | resources,
26 | lng: "en",
27 | interpolation: {
28 | escapeValue: false,
29 | },
30 | });
31 |
32 | export default i18n;
33 |
--------------------------------------------------------------------------------
/playground-next/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {},
11 | },
12 | plugins: [],
13 | };
14 | export default config;
15 |
--------------------------------------------------------------------------------
/playground-next/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/playground/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/playground/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13 |
14 | - Configure the top-level `parserOptions` property like this:
15 |
16 | ```js
17 | export default tseslint.config({
18 | languageOptions: {
19 | // other options...
20 | parserOptions: {
21 | project: ['./tsconfig.node.json', './tsconfig.app.json'],
22 | tsconfigRootDir: import.meta.dirname,
23 | },
24 | },
25 | })
26 | ```
27 |
28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
29 | - Optionally add `...tseslint.configs.stylisticTypeChecked`
30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
31 |
32 | ```js
33 | // eslint.config.js
34 | import react from 'eslint-plugin-react'
35 |
36 | export default tseslint.config({
37 | // Set the react version
38 | settings: { react: { version: '18.3' } },
39 | plugins: {
40 | // Add the react plugin
41 | react,
42 | },
43 | rules: {
44 | // other rules...
45 | // Enable its recommended rules
46 | ...react.configs.recommended.rules,
47 | ...react.configs['jsx-runtime'].rules,
48 | },
49 | })
50 | ```
51 |
--------------------------------------------------------------------------------
/playground/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/playground/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | React Cookie Manager Playground
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/playground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playground",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "i18next": "^24.2.2",
14 | "react": "^18.3.1",
15 | "react-dom": "^18.3.1",
16 | "react-i18next": "^15.4.0"
17 | },
18 | "devDependencies": {
19 | "@eslint/js": "^9.17.0",
20 | "@types/react": "^18.3.18",
21 | "@types/react-dom": "^18.3.5",
22 | "@vitejs/plugin-react": "^4.3.4",
23 | "autoprefixer": "^10.4.21",
24 | "eslint": "^9.17.0",
25 | "eslint-plugin-react-hooks": "^5.0.0",
26 | "eslint-plugin-react-refresh": "^0.4.16",
27 | "globals": "^15.14.0",
28 | "postcss": "^8.5.3",
29 | "tailwindcss": "^3.4.17",
30 | "typescript": "~5.6.2",
31 | "typescript-eslint": "^8.18.2",
32 | "vite": "^6.0.5"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/playground/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/playground/src/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | max-width: 1280px;
3 | margin: 0 auto;
4 | padding: 2rem;
5 | text-align: center;
6 | }
7 |
8 | .logo {
9 | height: 6em;
10 | padding: 1.5em;
11 | will-change: filter;
12 | transition: filter 300ms;
13 | }
14 | .logo:hover {
15 | filter: drop-shadow(0 0 2em #646cffaa);
16 | }
17 | .logo.react:hover {
18 | filter: drop-shadow(0 0 2em #61dafbaa);
19 | }
20 |
21 | @keyframes logo-spin {
22 | from {
23 | transform: rotate(0deg);
24 | }
25 | to {
26 | transform: rotate(360deg);
27 | }
28 | }
29 |
30 | @media (prefers-reduced-motion: no-preference) {
31 | a:nth-of-type(2) .logo {
32 | animation: logo-spin infinite 20s linear;
33 | }
34 | }
35 |
36 | .card {
37 | padding: 2em;
38 | }
39 |
40 | .read-the-docs {
41 | color: #888;
42 | }
43 |
--------------------------------------------------------------------------------
/playground/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useCookieConsent } from "../../dist";
2 | import "./App.css";
3 | import reactLogo from "./assets/react.svg";
4 | import { useState, useEffect } from "react";
5 |
6 | function App() {
7 | const { showConsentBanner, detailedConsent } = useCookieConsent();
8 | const [consentStatus, setConsentStatus] = useState("Checking...");
9 |
10 | // Track consent status changes
11 | useEffect(() => {
12 | if (detailedConsent) {
13 | const status = detailedConsent.Advertising.consented
14 | ? "Accepted"
15 | : "Not Accepted";
16 | setConsentStatus(status);
17 | } else {
18 | setConsentStatus("Not Set");
19 | }
20 | }, [detailedConsent]);
21 |
22 | return (
23 | <>
24 |
37 | React Cookie Manager Playground
38 |
39 |
40 |
Cookie Consent Status
41 |
42 | Marketing/Advertising Cookies: {consentStatus}
43 |
44 |
45 |
46 |
47 |
48 |
YouTube Video Embed Test
49 |
50 | This YouTube video is embedded directly to observe what happens when
51 | cookies haven't been accepted yet.
52 |
53 |
54 | {/* Direct YouTube embed without conditional rendering */}
55 |
56 |
65 |
66 |
67 |
76 |
77 |
78 | >
79 | );
80 | }
81 |
82 | export default App;
83 |
--------------------------------------------------------------------------------
/playground/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/playground/src/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/playground/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 | line-height: 1.5;
4 | font-weight: 400;
5 |
6 | color-scheme: light dark;
7 | color: rgba(255, 255, 255, 0.87);
8 | background-color: #242424;
9 |
10 | font-synthesis: none;
11 | text-rendering: optimizeLegibility;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | }
15 |
16 | a {
17 | font-weight: 500;
18 | color: #646cff;
19 | text-decoration: inherit;
20 | }
21 | a:hover {
22 | color: #535bf2;
23 | }
24 |
25 | body {
26 | margin: 0;
27 | display: flex;
28 | place-items: center;
29 | min-width: 320px;
30 | min-height: 100vh;
31 | }
32 |
33 | h1 {
34 | font-size: 3.2em;
35 | line-height: 1.1;
36 | }
37 |
38 | button {
39 | border-radius: 8px;
40 | border: 1px solid transparent;
41 | padding: 0.6em 1.2em;
42 | font-size: 1em;
43 | font-weight: 500;
44 | font-family: inherit;
45 | background-color: #1a1a1a;
46 | cursor: pointer;
47 | transition: border-color 0.25s;
48 | }
49 | button:hover {
50 | border-color: #646cff;
51 | }
52 | button:focus,
53 | button:focus-visible {
54 | outline: 4px auto -webkit-focus-ring-color;
55 | }
56 |
57 | @media (prefers-color-scheme: light) {
58 | :root {
59 | color: #213547;
60 | background-color: #ffffff;
61 | }
62 | a:hover {
63 | color: #747bff;
64 | }
65 | button {
66 | background-color: #f9f9f9;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/playground/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { default as i18n, default as i18next } from "i18next";
2 | import { StrictMode } from "react";
3 | import { createRoot } from "react-dom/client";
4 | import { initReactI18next } from "react-i18next";
5 | import { CookieManager } from "../../dist/";
6 | import "../../dist/style.css";
7 | import { CookieCategories } from "../../dist/types/types";
8 | import App from "./App.tsx";
9 | import "./index.css";
10 | import "./globals.css";
11 |
12 | const useI18next = true;
13 | const translationI18NextPrefix = "cookies";
14 |
15 | const Translations = {
16 | title: "Would You Like A Cookie? 🍪",
17 | message:
18 | "We value your privacy. Choose which cookies you want to allow. Essential cookies are always enabled as they are necessary for the website to function properly.",
19 | buttonText: "Accept All",
20 | declineButtonText: "Decline All",
21 | manageButtonText: "Manage Cookies",
22 | privacyPolicyText: "Privacy Policy",
23 | };
24 |
25 | i18n.use(initReactI18next).init({
26 | resources: {
27 | en: {
28 | translation: {
29 | [translationI18NextPrefix]: {
30 | title: "Would You Like A Cookie? 🍪",
31 | },
32 | },
33 | },
34 | pl: {
35 | translation: {
36 | [translationI18NextPrefix]: {
37 | title: "Chcesz ciasteczko? 🍪",
38 | manageCookiesStatus: "Status: {{status}} na dzień {{date}}",
39 | manageCookiesStatusConsented: "Zgoda",
40 | manageCookiesStatusDeclined: "Odmowa",
41 | },
42 | },
43 | },
44 | },
45 | lng: "en",
46 | });
47 |
48 | createRoot(document.getElementById("root")!).render(
49 |
50 | {
60 | if (preferences) {
61 | console.log("Cookie preferences updated:", preferences);
62 | }
63 | }}
64 | >
65 |
66 |
67 |
68 | );
69 |
--------------------------------------------------------------------------------
/playground/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/playground/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | };
9 |
--------------------------------------------------------------------------------
/playground/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true
24 | },
25 | "include": ["src"]
26 | }
27 |
--------------------------------------------------------------------------------
/playground/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/playground/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2022",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedSideEffectImports": true
22 | },
23 | "include": ["vite.config.ts"]
24 | }
25 |
--------------------------------------------------------------------------------
/playground/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vite.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | "postcss-prefix-selector": {
4 | prefix: ".cookie-manager", // We need to scope all our styles with our own class
5 | transform(prefix, selector) {
6 | if (selector.startsWith("html")) return prefix;
7 | return `${prefix} ${selector}`;
8 | },
9 | },
10 | "tailwindcss/nesting": {},
11 | tailwindcss: {},
12 | autoprefixer: {
13 | // Only add necessary prefixes
14 | flexbox: "no-2009",
15 | grid: "autoplace",
16 | },
17 | ...(process.env.NODE_ENV === "production"
18 | ? {
19 | cssnano: {
20 | preset: [
21 | "default",
22 | {
23 | discardComments: { removeAll: true },
24 | normalizeWhitespace: true,
25 | minifyFontValues: { removeQuotes: true },
26 | colormin: true,
27 | reduceIdents: true,
28 | mergeRules: true,
29 | mergeLonghand: true,
30 | discardDuplicates: true,
31 | discardEmpty: true,
32 | },
33 | ],
34 | },
35 | }
36 | : {}),
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/scripts/postinstall.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const postinstall = () => {
4 | console.log(`
5 | \x1b[1m🍪 Thank you for installing react-cookie-manager!\x1b[0m
6 |
7 | \x1b[36mWant to become 100% GDPR compliant for free? 🇪🇺\x1b[0m
8 |
9 | Visit \x1b[1mhttps://cookiekit.io\x1b[0m to:
10 | - Track and store user consent (required for full GDPR compliance)
11 | - Get real-time analytics
12 | - And much more!
13 |
14 | \x1b[32mBest of all, it's completely free! 🎉\x1b[0m
15 |
16 | \x1b[90m-------------------------------------------\x1b[0m
17 | `);
18 | };
19 |
20 | // Run the script
21 | postinstall();
22 |
23 | // For CommonJS compatibility
24 | if (typeof exports !== "undefined") {
25 | module.exports = postinstall;
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/CookieConsenter.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { createPortal } from "react-dom";
3 | import {
4 | CookieConsenterProps,
5 | CookieCategories,
6 | DetailedCookieConsent,
7 | } from "../types/types";
8 | import { TFunction } from "../utils/translations";
9 | import { ManageConsent } from "./ManageConsent";
10 | import { cn } from "../utils/cn";
11 |
12 | const useIsMobile = () => {
13 | const [isMobile, setIsMobile] = useState(false);
14 |
15 | useEffect(() => {
16 | const checkIsMobile = () => {
17 | setIsMobile(window.innerWidth < 640); // matches Tailwind's sm breakpoint
18 | };
19 |
20 | checkIsMobile();
21 | window.addEventListener("resize", checkIsMobile);
22 | return () => window.removeEventListener("resize", checkIsMobile);
23 | }, []);
24 |
25 | return isMobile;
26 | };
27 |
28 | const MobileModal: React.FC<
29 | Omit & {
30 | tFunction: TFunction;
31 | handleAccept: (e: React.MouseEvent) => void;
32 | handleDecline: (e: React.MouseEvent) => void;
33 | handleManage: (e: React.MouseEvent) => void;
34 | isExiting: boolean;
35 | isEntering: boolean;
36 | isManaging: boolean;
37 | handleSavePreferences: (categories: CookieCategories) => void;
38 | handleCancelManage: () => void;
39 | initialPreferences?: CookieCategories;
40 | detailedConsent?: DetailedCookieConsent | null;
41 | classNames?: CookieConsenterProps["classNames"];
42 | }
43 | > = ({
44 | showManageButton,
45 | privacyPolicyUrl,
46 | theme,
47 | tFunction,
48 | handleAccept,
49 | handleDecline,
50 | handleManage,
51 | isExiting,
52 | isEntering,
53 | isManaging,
54 | handleSavePreferences,
55 | handleCancelManage,
56 | displayType = "banner",
57 | initialPreferences,
58 | detailedConsent,
59 | classNames,
60 | }) => {
61 | const title = tFunction("title");
62 | return (
63 |
64 | {displayType === "modal" && (
65 |
66 | )}
67 |
78 |
87 | {isManaging ? (
88 |
97 | ) : (
98 |
99 | {title && (
100 |
106 | {title}
107 |
108 | )}
109 |
115 | {tFunction("message")}
116 |
117 |
118 |
130 |
145 | {showManageButton && (
146 |
158 | )}
159 |
160 | {privacyPolicyUrl && (
161 |
176 | {tFunction("privacyPolicyText")}
177 |
178 | )}
179 |
180 | )}
181 |
182 |
183 |
184 | );
185 | };
186 |
187 | const CookieConsenter: React.FC<
188 | CookieConsenterProps & { tFunction: TFunction }
189 | > = ({
190 | showManageButton = true,
191 | privacyPolicyUrl,
192 | displayType = "popup",
193 | theme = "light",
194 | tFunction,
195 | onAccept,
196 | onDecline,
197 | onManage,
198 | initialPreferences = {
199 | Analytics: false,
200 | Social: false,
201 | Advertising: false,
202 | },
203 | detailedConsent,
204 | isManaging = false,
205 | classNames,
206 | }) => {
207 | const [isExiting, setIsExiting] = useState(false);
208 | const [isEntering, setIsEntering] = useState(true);
209 | const [shouldRender, setShouldRender] = useState(true);
210 | const isMobile = useIsMobile();
211 |
212 | useEffect(() => {
213 | setTimeout(() => {
214 | setIsEntering(false);
215 | }, 50);
216 | }, []);
217 |
218 | useEffect(() => {
219 | if (isExiting) {
220 | const timer = setTimeout(() => {
221 | setShouldRender(false);
222 | }, 500); // Match the duration of the exit animation
223 | return () => clearTimeout(timer);
224 | }
225 | }, [isExiting]);
226 |
227 | const handleAcceptClick = (e: React.MouseEvent) => {
228 | e.preventDefault();
229 | setIsExiting(true);
230 | setTimeout(() => {
231 | if (onAccept) onAccept();
232 | }, 500);
233 | };
234 |
235 | const handleDeclineClick = (e: React.MouseEvent) => {
236 | e.preventDefault();
237 | setIsExiting(true);
238 | setTimeout(() => {
239 | if (onDecline) onDecline();
240 | }, 500);
241 | };
242 |
243 | const handleManageClick = (e: React.MouseEvent) => {
244 | e.preventDefault();
245 | if (onManage) onManage();
246 | };
247 |
248 | const handleSavePreferences = (categories: CookieCategories) => {
249 | setIsExiting(true);
250 | setTimeout(() => {
251 | if (onManage) {
252 | onManage(categories);
253 | }
254 | }, 500);
255 | };
256 |
257 | const handleCancelManage = () => {
258 | setIsExiting(true);
259 | setTimeout(() => {
260 | if (onManage) onManage();
261 | }, 500);
262 | };
263 |
264 | if (!shouldRender) return null;
265 |
266 | // If isManaging is true, don't render the consenter
267 | if (isManaging) {
268 | return null;
269 | }
270 |
271 | // On mobile, always render the MobileModal regardless of displayType
272 | if (isMobile) {
273 | return createPortal(
274 | ,
294 | document.body
295 | );
296 | }
297 |
298 | const acceptButtonClasses = classNames?.acceptButton
299 | ? cn(classNames.acceptButton)
300 | : cn(
301 | "px-3 py-1.5 text-xs font-medium rounded-md",
302 | "bg-blue-500 hover:bg-blue-600 text-white",
303 | "transition-all duration-200",
304 | "hover:scale-105 focus-visible:outline-none focus:outline-none",
305 | "focus-visible:outline-transparent focus:outline-transparent",
306 | displayType === "popup" ? "flex-1" : ""
307 | );
308 |
309 | const declineButtonClasses = classNames?.declineButton
310 | ? cn(classNames.declineButton)
311 | : cn(
312 | "px-3 py-1.5 text-xs font-medium rounded-md",
313 | theme === "light"
314 | ? "bg-gray-200 hover:bg-gray-300 text-gray-800"
315 | : "bg-gray-800 hover:bg-gray-700 text-gray-300",
316 | "transition-all duration-200",
317 | "hover:scale-105 focus-visible:outline-none focus:outline-none",
318 | "focus-visible:outline-transparent focus:outline-transparent",
319 | displayType === "popup" ? "flex-1" : ""
320 | );
321 |
322 | const manageButtonClasses = classNames?.manageButton
323 | ? cn(classNames.manageButton)
324 | : cn(
325 | "px-3 py-1.5 text-xs font-medium rounded-md",
326 | "border border-blue-500 text-blue-500",
327 | "bg-transparent",
328 | "hover:text-blue-600 hover:border-blue-600",
329 | "transition-all duration-200",
330 | "hover:scale-105 focus-visible:outline-none focus:outline-none",
331 | "focus-visible:outline-transparent focus:outline-transparent",
332 | displayType === "popup" ? "flex-1" : ""
333 | );
334 |
335 | const privacyLinkClasses = classNames?.privacyPolicyLink
336 | ? cn(classNames.privacyPolicyLink)
337 | : cn(
338 | "text-xs font-medium",
339 | theme === "light"
340 | ? "text-gray-500 hover:text-gray-700"
341 | : "text-gray-400 hover:text-gray-200",
342 | "transition-colors duration-200"
343 | );
344 |
345 | const modalBaseClasses = classNames?.modalContainer
346 | ? cn(classNames.modalContainer)
347 | : cn(
348 | "fixed inset-0 flex items-center justify-center p-4",
349 | theme === "light"
350 | ? "bg-black/20 backdrop-blur-sm"
351 | : "bg-black/40 backdrop-blur-sm",
352 | "transition-all duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]",
353 | "z-[99999]",
354 | isExiting ? "opacity-0" : isEntering ? "opacity-0" : "opacity-100"
355 | );
356 |
357 | const modalContentClasses = classNames?.modalContent
358 | ? cn(classNames.modalContent)
359 | : cn(
360 | "w-full max-w-lg rounded-xl p-6",
361 | theme === "light"
362 | ? "bg-white/95 ring-2 ring-gray-200"
363 | : "bg-black/95 ring-1 ring-white/10",
364 | isExiting ? "scale-95" : isEntering ? "scale-95" : "scale-100",
365 | "transition-transform duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]"
366 | );
367 |
368 | const modalTitleClasses = classNames?.modalTitle
369 | ? cn(classNames.modalTitle)
370 | : cn(
371 | "text-lg font-semibold mb-3",
372 | theme === "light" ? "text-gray-900" : "text-white"
373 | );
374 |
375 | const modalMessageClasses = classNames?.modalMessage
376 | ? cn(classNames.modalMessage)
377 | : cn(
378 | "text-sm font-medium mb-6",
379 | theme === "light" ? "text-gray-700" : "text-gray-200"
380 | );
381 |
382 | const popupBaseClasses = classNames?.popupContainer
383 | ? cn(classNames.popupContainer)
384 | : cn(
385 | "fixed bottom-4 left-4 w-80",
386 | theme === "light"
387 | ? "bg-white/95 ring-1 ring-black/10 shadow-lg"
388 | : "bg-black/95 ring-1 ring-white/10",
389 | "rounded-lg backdrop-blur-sm backdrop-saturate-150",
390 | "transition-all duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]",
391 | "z-[99999] hover:-translate-y-2",
392 | isExiting
393 | ? "opacity-0 scale-95"
394 | : isEntering
395 | ? "opacity-0 scale-95"
396 | : "opacity-100 scale-100"
397 | );
398 |
399 | const bannerBaseClasses = classNames?.bannerContainer
400 | ? cn(classNames.bannerContainer)
401 | : cn(
402 | "fixed bottom-4 left-1/2 -translate-x-1/2 w-full md:max-w-2xl",
403 | theme === "light"
404 | ? "bg-white/95 border border-black/10 shadow-lg"
405 | : "bg-black/95 ring-1 ring-white/10",
406 | "rounded-lg backdrop-blur-sm backdrop-saturate-150",
407 | "transition-all duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]",
408 | "z-[99999] hover:-translate-y-2",
409 | isExiting
410 | ? "opacity-0 transform translate-y-full"
411 | : isEntering
412 | ? "opacity-0 transform translate-y-full"
413 | : "opacity-100 transform translate-y-0"
414 | );
415 |
416 | const bannerContentClasses = classNames?.bannerContent
417 | ? cn(classNames.bannerContent)
418 | : cn(
419 | "flex flex-col gap-4 p-4",
420 | theme === "light" ? "text-gray-600" : "text-gray-300"
421 | );
422 |
423 | const popupContentClasses = classNames?.popupContent
424 | ? cn(classNames.popupContent)
425 | : cn(
426 | "flex flex-col items-start gap-4 p-4",
427 | theme === "light" ? "text-gray-600" : "text-gray-300"
428 | );
429 |
430 | const bannerTitleClasses = classNames?.bannerTitle
431 | ? cn(classNames.bannerTitle)
432 | : cn(
433 | "text-sm font-semibold mb-1",
434 | theme === "light" ? "text-gray-900" : "text-white"
435 | );
436 |
437 | const popupTitleClasses = classNames?.popupTitle
438 | ? cn(classNames.popupTitle)
439 | : cn(
440 | "text-sm font-semibold mb-2",
441 | theme === "light" ? "text-gray-900" : "text-white"
442 | );
443 |
444 | const bannerMessageClasses = classNames?.bannerMessage
445 | ? cn(classNames.bannerMessage)
446 | : cn(
447 | "text-xs sm:text-sm font-medium text-center sm:text-left",
448 | theme === "light" ? "text-gray-700" : "text-gray-200"
449 | );
450 |
451 | const popupMessageClasses = classNames?.popupMessage
452 | ? cn(classNames.popupMessage)
453 | : cn(
454 | "text-xs font-medium",
455 | theme === "light" ? "text-gray-700" : "text-gray-200"
456 | );
457 |
458 | const getBaseClasses = () => {
459 | switch (displayType) {
460 | case "modal":
461 | return modalBaseClasses;
462 | case "popup":
463 | return popupBaseClasses;
464 | default:
465 | return bannerBaseClasses;
466 | }
467 | };
468 |
469 | const getContentClasses = () => {
470 | switch (displayType) {
471 | case "modal":
472 | return modalContentClasses;
473 | case "popup":
474 | return popupContentClasses;
475 | default:
476 | return bannerContentClasses;
477 | }
478 | };
479 |
480 | const getTitleClasses = () => {
481 | switch (displayType) {
482 | case "modal":
483 | return modalTitleClasses;
484 | case "popup":
485 | return popupTitleClasses;
486 | default:
487 | return bannerTitleClasses;
488 | }
489 | };
490 |
491 | const getMessageClasses = () => {
492 | switch (displayType) {
493 | case "modal":
494 | return modalMessageClasses;
495 | case "popup":
496 | return popupMessageClasses;
497 | default:
498 | return bannerMessageClasses;
499 | }
500 | };
501 |
502 | const renderContent = () => {
503 | const title = tFunction("title");
504 | if (displayType === "banner") {
505 | return (
506 |
507 |
508 | {title &&
{title}
}
509 |
{tFunction("message")}
510 |
511 |
512 | {privacyPolicyUrl && (
513 |
519 | {tFunction("privacyPolicyText")}
520 |
521 | )}
522 |
523 | {showManageButton && (
524 |
530 | )}
531 |
537 |
543 |
544 |
545 |
546 | );
547 | }
548 | return (
549 |
550 | {title &&
{title}
}
551 |
{tFunction("message")}
552 |
553 | );
554 | };
555 |
556 | const renderButtons = () => {
557 | if (displayType === "popup") {
558 | return (
559 |
560 |
561 |
567 |
570 |
571 |
591 |
592 | );
593 | }
594 |
595 | if (displayType === "modal") {
596 | return (
597 |
598 |
599 | {privacyPolicyUrl && (
600 |
606 | {tFunction("privacyPolicyText")}
607 |
608 | )}
609 |
610 | {showManageButton && (
611 |
617 | )}
618 |
624 |
630 |
631 |
632 |
633 | );
634 | }
635 |
636 | return null;
637 | };
638 |
639 | const content = (
640 |
641 |
642 | {displayType === "modal" ? (
643 |
644 | {renderContent()}
645 | {renderButtons()}
646 |
647 | ) : (
648 |
649 | {renderContent()}
650 | {renderButtons()}
651 |
652 | )}
653 |
654 |
655 | );
656 |
657 | return createPortal(content, document.body);
658 | };
659 |
660 | export default CookieConsenter;
661 |
--------------------------------------------------------------------------------
/src/components/FloatingCookieButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { CookieConsenterClassNames } from "../types/types";
3 | import { cn } from "../utils/cn";
4 |
5 | interface FloatingCookieButtonProps {
6 | theme?: "light" | "dark";
7 | onClick: () => void;
8 | onClose?: () => void;
9 | classNames?: CookieConsenterClassNames;
10 | }
11 |
12 | export const FloatingCookieButton: React.FC = ({
13 | theme = "light",
14 | onClick,
15 | onClose,
16 | classNames,
17 | }) => {
18 | const [isHovered, setIsHovered] = useState(false);
19 |
20 | return (
21 | setIsHovered(true)}
24 | onMouseLeave={() => setIsHovered(false)}
25 | className={
26 | classNames?.floatingButton
27 | ? cn(classNames.floatingButton)
28 | : cn(`
29 | fixed bottom-6 left-6 z-[99999]
30 | w-12 h-12 rounded-full
31 | flex items-center justify-center
32 | transition-all duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]
33 | hover:scale-110 focus:outline-none
34 | group cursor-pointer
35 | ${
36 | theme === "light"
37 | ? "bg-white/95 shadow-lg ring-1 ring-black/10 text-gray-700 hover:text-gray-900"
38 | : "bg-black/95 shadow-lg ring-1 ring-white/10 text-gray-300 hover:text-white"
39 | }
40 | `)
41 | }
42 | style={{
43 | animation:
44 | "slide-in-bottom 0.5s cubic-bezier(0.32, 0.72, 0, 1) forwards",
45 | }}
46 | aria-label="Manage cookie preferences"
47 | role="button"
48 | tabIndex={0}
49 | onKeyDown={(e) => {
50 | if (e.key === "Enter" || e.key === " ") {
51 | onClick();
52 | }
53 | }}
54 | >
55 | {/* Close button */}
56 | {isHovered && (
57 |
94 | )}
95 |
109 |
169 |
170 | );
171 | };
172 |
--------------------------------------------------------------------------------
/src/components/ManageConsent.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import {
3 | CookieCategories,
4 | DetailedCookieConsent,
5 | CookieConsenterClassNames,
6 | } from "../types/types";
7 | import { TFunction } from "../utils/translations";
8 | import { cn } from "../utils/cn";
9 |
10 | interface ManageConsentProps {
11 | theme?: "light" | "dark";
12 | tFunction: TFunction;
13 | onSave: (categories: CookieCategories) => void;
14 | onCancel?: () => void;
15 | initialPreferences?: CookieCategories;
16 | detailedConsent?: DetailedCookieConsent | null;
17 | classNames?: CookieConsenterClassNames;
18 | }
19 |
20 | export const ManageConsent: React.FC = ({
21 | theme = "light",
22 | tFunction,
23 | onSave,
24 | onCancel,
25 | initialPreferences = {
26 | Analytics: false,
27 | Social: false,
28 | Advertising: false,
29 | },
30 | detailedConsent,
31 | classNames,
32 | }) => {
33 | const [consent, setConsent] = useState(initialPreferences);
34 |
35 | const handleToggle = (category: keyof CookieCategories) => {
36 | setConsent((prev) => ({
37 | ...prev,
38 | [category]: !prev[category],
39 | }));
40 | };
41 |
42 | const handleSave = () => {
43 | onSave(consent);
44 | };
45 |
46 | const formatDate = (timestamp: string) => {
47 | try {
48 | const date = new Date(timestamp);
49 | return date.toLocaleString(undefined, {
50 | year: "numeric",
51 | month: "short",
52 | day: "numeric",
53 | hour: "2-digit",
54 | minute: "2-digit",
55 | });
56 | } catch (e) {
57 | return "Invalid date";
58 | }
59 | };
60 |
61 | const renderConsentStatus = (category: keyof CookieCategories) => {
62 | if (!detailedConsent || !detailedConsent[category]) return null;
63 |
64 | const status = detailedConsent[category];
65 | return (
66 |
76 | {tFunction("manageCookiesStatus", {
77 | status: status.consented
78 | ? tFunction("manageCookiesStatusConsented")
79 | : tFunction("manageCookiesStatusDeclined"),
80 | date: formatDate(status.timestamp),
81 | })}
82 |
83 | );
84 | };
85 |
86 | return (
87 |
94 |
95 |
105 | {tFunction("manageTitle")}
106 |
107 |
117 | {tFunction("manageMessage")}
118 |
119 |
120 |
121 |
122 | {/* Essential Cookies - Always enabled */}
123 |
130 |
131 |
141 | {tFunction("manageEssentialTitle")}
142 |
143 |
153 | {tFunction("manageEssentialSubtitle")}
154 |
155 |
165 | {tFunction("manageEssentialStatus")}
166 |
167 |
168 |
175 | {tFunction("manageEssentialStatusButtonText")}
176 |
177 |
178 |
179 | {/* Analytics Cookies */}
180 |
187 |
188 |
198 | {tFunction("manageAnalyticsTitle")}
199 |
200 |
210 | {tFunction("manageAnalyticsSubtitle")}
211 |
212 | {renderConsentStatus("Analytics")}
213 |
214 |
240 |
241 |
242 | {/* Social Cookies */}
243 |
250 |
251 |
261 | {tFunction("manageSocialTitle")}
262 |
263 |
273 | {tFunction("manageSocialSubtitle")}
274 |
275 | {renderConsentStatus("Social")}
276 |
277 |
303 |
304 |
305 | {/* Advertising Cookies */}
306 |
313 |
314 |
324 | {tFunction("manageAdvertTitle")}
325 |
326 |
336 | {tFunction("manageAdvertSubtitle")}
337 |
338 | {renderConsentStatus("Advertising")}
339 |
340 |
367 |
368 |
369 |
370 |
371 | {onCancel && (
372 |
388 | )}
389 |
399 |
400 |
401 |
421 |
422 | );
423 | };
424 |
--------------------------------------------------------------------------------
/src/context/CookieConsentContext.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | createContext,
3 | useContext,
4 | useState,
5 | useEffect,
6 | useMemo,
7 | useRef,
8 | } from "react";
9 | import { createPortal } from "react-dom";
10 | import CookieConsenter from "../components/CookieConsenter";
11 | import { FloatingCookieButton } from "../components/FloatingCookieButton";
12 | import type {
13 | CookieCategories,
14 | CookieConsenterProps,
15 | DetailedCookieConsent,
16 | TranslationObject,
17 | TranslationFunction,
18 | } from "../types/types";
19 | import { ManageConsent } from "../components/ManageConsent";
20 | import { getBlockedHosts, getBlockedKeywords } from "../utils/tracker-utils";
21 | import { createTFunction } from "../utils/translations";
22 | import { CookieBlockingManager } from "../utils/cookie-blocking";
23 | import { setCookie, getCookie, deleteCookie } from "../utils/cookie-utils";
24 | import {
25 | generateSessionId,
26 | postSessionToAnalytics,
27 | } from "../utils/session-utils";
28 |
29 | // Helper function to check if running on localhost
30 | const isLocalhost = (): boolean => {
31 | if (typeof window !== "undefined") {
32 | const hostname = window.location.hostname;
33 | return (
34 | hostname === "localhost" ||
35 | hostname === "127.0.0.1" ||
36 | hostname.startsWith("192.168.") ||
37 | hostname.startsWith("10.")
38 | );
39 | }
40 | return false;
41 | };
42 |
43 | // Helper function to post to analytics if not on localhost
44 | const postToAnalyticsIfNotLocalhost = async (
45 | cookieKitId: string,
46 | sessionId: string,
47 | action?: string,
48 | preferences?: CookieCategories,
49 | userId?: string
50 | ) => {
51 | if (isLocalhost()) {
52 | console.log(
53 | "[CookieKit] Running on localhost - consent data will be sent when deployed to production"
54 | );
55 | return;
56 | }
57 |
58 | await postSessionToAnalytics(
59 | cookieKitId,
60 | sessionId,
61 | action,
62 | preferences,
63 | userId
64 | );
65 | };
66 |
67 | interface CookieConsentContextValue {
68 | hasConsent: boolean | null;
69 | isDeclined: boolean;
70 | detailedConsent: DetailedCookieConsent | null;
71 | showConsentBanner: () => void;
72 | acceptCookies: () => void;
73 | declineCookies: () => void;
74 | updateDetailedConsent: (preferences: CookieCategories) => void;
75 | }
76 |
77 | const CookieManagerContext = createContext(
78 | null
79 | );
80 |
81 | export interface CookieManagerProps
82 | extends Omit {
83 | children: React.ReactNode;
84 | cookieKey?: string;
85 | cookieKitId?: string;
86 | userId?: string;
87 | onManage?: (preferences?: CookieCategories) => void;
88 | onAccept?: () => void;
89 | onDecline?: () => void;
90 | disableAutomaticBlocking?: boolean;
91 | blockedDomains?: string[];
92 | expirationDays?: number;
93 | /**
94 | * Translations that will be used in the consent UI. It can be one of:
95 | * 1. **TranslationObject**: An object with keys for each TranslationKey, e.g.:
96 | * ```
97 | * {
98 | * title: 'My own consent title',
99 | * message: 'My own consent message',
100 | * // other keys if needed
101 | * }
102 | * ```
103 | * 2. **TranslationFunction**: A function that takes a key with params and returns a string. Useful for i18n libraries where TFunction can be passed like follows:
104 | * ```ts
105 | * const { t } = useTranslation();
106 | * return
107 | * ```
108 | *
109 | * By default it uses English translations specified in TranslationKey defaults.
110 | */
111 | translations?: TranslationObject | TranslationFunction;
112 | /**
113 | * Prefix for translation keys when using i18next, e.g.
114 | * ```ts
115 | * // typescript file
116 | * const { t } = useTranslation();
117 | *
118 | * ```
119 | * ```json
120 | * // {lng}.json
121 | * {
122 | * "cookieConsent": {
123 | * "title": "My own consent title",
124 | * "message": "My own consent message"
125 | * }
126 | * }
127 | * ```
128 | */
129 | translationI18NextPrefix?: string;
130 | enableFloatingButton?: boolean;
131 | theme?: "light" | "dark";
132 | }
133 |
134 | const createConsentStatus = (consented: boolean) => ({
135 | consented,
136 | timestamp: new Date().toISOString(),
137 | });
138 |
139 | const createDetailedConsent = (consented: boolean): DetailedCookieConsent => ({
140 | Analytics: createConsentStatus(consented),
141 | Social: createConsentStatus(consented),
142 | Advertising: createConsentStatus(consented),
143 | });
144 |
145 | export const CookieManager: React.FC = ({
146 | children,
147 | cookieKey = "cookie-consent",
148 | cookieKitId,
149 | userId,
150 | translations,
151 | translationI18NextPrefix,
152 | onManage,
153 | onAccept,
154 | onDecline,
155 | disableAutomaticBlocking = false,
156 | blockedDomains = [],
157 | expirationDays = 365,
158 | enableFloatingButton = false,
159 | theme = "light",
160 | ...props
161 | }) => {
162 | const [isVisible, setIsVisible] = useState(false);
163 | const [showManageConsent, setShowManageConsent] = useState(false);
164 | const [isFloatingButtonVisible, setIsFloatingButtonVisible] = useState(false);
165 | const hasPostedSession = useRef(false);
166 | const isGeneratingSession = useRef(false);
167 | const tFunction = useMemo(
168 | () => createTFunction(translations, translationI18NextPrefix),
169 | [translations, translationI18NextPrefix]
170 | );
171 |
172 | const [detailedConsent, setDetailedConsent] =
173 | useState(() => {
174 | const storedConsent = getCookie(cookieKey);
175 | if (storedConsent) {
176 | try {
177 | const parsedConsent = JSON.parse(
178 | storedConsent
179 | ) as DetailedCookieConsent;
180 |
181 | // Check if consent has expired
182 | const oldestTimestamp = Math.min(
183 | ...Object.values(parsedConsent).map((status) =>
184 | new Date(status.timestamp).getTime()
185 | )
186 | );
187 |
188 | const expirationTime =
189 | oldestTimestamp + expirationDays * 24 * 60 * 60 * 1000;
190 |
191 | if (Date.now() > expirationTime) {
192 | deleteCookie(cookieKey);
193 | return null;
194 | }
195 |
196 | return parsedConsent;
197 | } catch (e) {
198 | return null;
199 | }
200 | }
201 | return null;
202 | });
203 |
204 | const hasConsent = detailedConsent
205 | ? Object.values(detailedConsent).some((status) => status.consented)
206 | : null;
207 |
208 | // Use the CookieBlockingManager
209 | const cookieBlockingManager = useRef(null);
210 |
211 | // Initialize session ID if cookieKitId is provided
212 | useEffect(() => {
213 | let isMounted = true;
214 | let isInitializing = false;
215 |
216 | const initializeSessionId = async () => {
217 | if (!cookieKitId || isInitializing) return;
218 |
219 | isInitializing = true;
220 | const sessionKey = `${cookieKey}-session`;
221 | let sessionId = getCookie(sessionKey);
222 |
223 | if (!sessionId) {
224 | try {
225 | sessionId = await generateSessionId(cookieKitId);
226 | if (!isMounted) return;
227 | setCookie(sessionKey, sessionId, 1);
228 | const savedSessionId = getCookie(sessionKey);
229 | if (savedSessionId && isMounted) {
230 | await postToAnalyticsIfNotLocalhost(
231 | cookieKitId,
232 | sessionId,
233 | undefined,
234 | undefined,
235 | userId
236 | );
237 | }
238 | } catch (error) {
239 | console.error("Error in session initialization:", error);
240 | }
241 | } else {
242 | }
243 | };
244 |
245 | initializeSessionId();
246 |
247 | return () => {
248 | isMounted = false;
249 | isInitializing = false;
250 | };
251 | }, [cookieKitId, cookieKey, userId]);
252 |
253 | useEffect(() => {
254 | // Show banner if no consent decision has been made AND manage consent is not shown
255 | if (detailedConsent === null && !showManageConsent) {
256 | setIsVisible(true);
257 | }
258 |
259 | // Handle tracking blocking
260 | if (!disableAutomaticBlocking) {
261 | // Get current preferences
262 | const currentPreferences = detailedConsent
263 | ? {
264 | Analytics: detailedConsent.Analytics.consented,
265 | Social: detailedConsent.Social.consented,
266 | Advertising: detailedConsent.Advertising.consented,
267 | }
268 | : null;
269 |
270 | // Get blocked hosts and keywords based on preferences
271 | const blockedHosts = [
272 | ...getBlockedHosts(currentPreferences),
273 | ...blockedDomains,
274 | ];
275 |
276 | const blockedKeywords = [
277 | ...getBlockedKeywords(currentPreferences),
278 | ...blockedDomains,
279 | ];
280 |
281 | // Initialize or update cookie blocking
282 | if (blockedHosts.length > 0 || blockedKeywords.length > 0) {
283 | // Create a new manager if one doesn't exist
284 | if (!cookieBlockingManager.current) {
285 | cookieBlockingManager.current = new CookieBlockingManager();
286 | }
287 |
288 | // Initialize the manager with current blocked hosts and keywords
289 | cookieBlockingManager.current.initialize(blockedHosts, blockedKeywords);
290 | } else {
291 | // Clean up if no blocking is needed
292 | if (cookieBlockingManager.current) {
293 | cookieBlockingManager.current.cleanup();
294 | }
295 | }
296 | } else {
297 | // Clean up if blocking is disabled
298 | if (cookieBlockingManager.current) {
299 | cookieBlockingManager.current.cleanup();
300 | cookieBlockingManager.current = null;
301 | }
302 | }
303 |
304 | return () => {
305 | // Clean up on unmount
306 | if (cookieBlockingManager.current) {
307 | cookieBlockingManager.current.cleanup();
308 | }
309 | };
310 | }, [detailedConsent, disableAutomaticBlocking, blockedDomains]);
311 |
312 | const showConsentBanner = () => {
313 | if (!showManageConsent) {
314 | // Only show banner if manage consent is not shown
315 | setIsVisible(true);
316 | }
317 | };
318 |
319 | const acceptCookies = async () => {
320 | const newConsent = createDetailedConsent(true);
321 | setCookie(cookieKey, JSON.stringify(newConsent), expirationDays);
322 | setDetailedConsent(newConsent);
323 | setIsVisible(false);
324 | if (enableFloatingButton) {
325 | setIsFloatingButtonVisible(true);
326 | }
327 |
328 | if (cookieKitId) {
329 | const sessionKey = `${cookieKey}-session`;
330 | const sessionId = getCookie(sessionKey);
331 | if (sessionId) {
332 | await postToAnalyticsIfNotLocalhost(
333 | cookieKitId,
334 | sessionId,
335 | "accept",
336 | {
337 | Analytics: true,
338 | Social: true,
339 | Advertising: true,
340 | },
341 | userId
342 | );
343 | }
344 | }
345 |
346 | // Call the onAccept callback if provided
347 | if (onAccept) {
348 | onAccept();
349 | }
350 | };
351 |
352 | const declineCookies = async () => {
353 | const newConsent = createDetailedConsent(false);
354 | setCookie(cookieKey, JSON.stringify(newConsent), expirationDays);
355 | setDetailedConsent(newConsent);
356 | setIsVisible(false);
357 | if (enableFloatingButton) {
358 | setIsFloatingButtonVisible(true);
359 | }
360 |
361 | if (cookieKitId) {
362 | const sessionKey = `${cookieKey}-session`;
363 | const sessionId = getCookie(sessionKey);
364 | if (sessionId) {
365 | await postToAnalyticsIfNotLocalhost(
366 | cookieKitId,
367 | sessionId,
368 | "decline",
369 | {
370 | Analytics: false,
371 | Social: false,
372 | Advertising: false,
373 | },
374 | userId
375 | );
376 | }
377 | }
378 |
379 | // Call the onDecline callback if provided
380 | if (onDecline) {
381 | onDecline();
382 | }
383 | };
384 |
385 | const updateDetailedConsent = async (preferences: CookieCategories) => {
386 | const timestamp = new Date().toISOString();
387 | const newConsent: DetailedCookieConsent = {
388 | Analytics: { consented: preferences.Analytics, timestamp },
389 | Social: { consented: preferences.Social, timestamp },
390 | Advertising: { consented: preferences.Advertising, timestamp },
391 | };
392 | setCookie(cookieKey, JSON.stringify(newConsent), expirationDays);
393 | setDetailedConsent(newConsent);
394 | setShowManageConsent(false);
395 | if (enableFloatingButton) {
396 | setIsFloatingButtonVisible(true);
397 | }
398 |
399 | if (cookieKitId) {
400 | const sessionKey = `${cookieKey}-session`;
401 | const sessionId = getCookie(sessionKey);
402 | if (sessionId) {
403 | await postToAnalyticsIfNotLocalhost(
404 | cookieKitId,
405 | sessionId,
406 | "save_preferences",
407 | preferences,
408 | userId
409 | );
410 | }
411 | }
412 |
413 | if (onManage) {
414 | onManage(preferences);
415 | }
416 | };
417 |
418 | const handleManage = () => {
419 | setIsVisible(false);
420 | setShowManageConsent(true);
421 | setIsFloatingButtonVisible(false);
422 | };
423 |
424 | const handleCancelManage = () => {
425 | setShowManageConsent(false);
426 | if (enableFloatingButton && detailedConsent) {
427 | setIsFloatingButtonVisible(true);
428 | } else {
429 | setIsVisible(true);
430 | }
431 | };
432 |
433 | // Add effect to show floating button on mount if consent exists
434 | useEffect(() => {
435 | if (enableFloatingButton && detailedConsent) {
436 | setIsFloatingButtonVisible(true);
437 | }
438 |
439 | // Add event listener for custom event to show cookie settings
440 | const handleShowCookieConsent = () => {
441 | console.debug(
442 | "[CookieKit] Custom event triggered to show cookie settings"
443 | );
444 | if (detailedConsent) {
445 | setShowManageConsent(true);
446 | setIsFloatingButtonVisible(false);
447 | } else {
448 | setIsVisible(true);
449 | }
450 | };
451 |
452 | window.addEventListener("show-cookie-consent", handleShowCookieConsent);
453 |
454 | return () => {
455 | window.removeEventListener(
456 | "show-cookie-consent",
457 | handleShowCookieConsent
458 | );
459 | };
460 | }, [enableFloatingButton, detailedConsent]);
461 |
462 | const value: CookieConsentContextValue = {
463 | hasConsent,
464 | isDeclined: hasConsent === false,
465 | detailedConsent,
466 | showConsentBanner,
467 | acceptCookies,
468 | declineCookies,
469 | updateDetailedConsent,
470 | };
471 |
472 | return (
473 |
474 | {children}
475 | {isVisible && (
476 |
495 | )}
496 | {showManageConsent &&
497 | createPortal(
498 | ,
527 | document.body
528 | )}
529 | {isFloatingButtonVisible &&
530 | !isVisible &&
531 | !showManageConsent &&
532 | createPortal(
533 |
534 | {
537 | setShowManageConsent(true);
538 | setIsFloatingButtonVisible(false);
539 | }}
540 | onClose={() => {
541 | setIsFloatingButtonVisible(false);
542 | }}
543 | classNames={props.classNames}
544 | />
545 |
,
546 | document.body
547 | )}
548 |
549 | );
550 | };
551 |
552 | export const useCookieConsent = () => {
553 | const context = useContext(CookieManagerContext);
554 | if (!context) {
555 | throw new Error("useCookieConsent must be used within a CookieManager");
556 | }
557 | return context;
558 | };
559 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import "./styles/tailwind.css";
2 | export { default as CookieConsenter } from "./components/CookieConsenter";
3 | export type { CookieConsenterProps } from "./types/types";
4 | export {
5 | CookieManager,
6 | useCookieConsent,
7 | } from "./context/CookieConsentContext";
8 | export type { CookieManagerProps } from "./context/CookieConsentContext";
9 |
--------------------------------------------------------------------------------
/src/styles/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | /* Cookie consent blocked content styles */
6 | @layer components {
7 | .cookie-consent-blocked-iframe {
8 | width: 100%;
9 | height: 100%;
10 | min-height: 200px;
11 | display: flex;
12 | align-items: center;
13 | justify-content: center;
14 | position: absolute;
15 | top: 0;
16 | left: 0;
17 | z-index: 100; /* Ensure it's above the iframe */
18 | }
19 |
20 | .cookie-consent-blocked-content {
21 | text-align: center;
22 | padding: 24px;
23 | color: #f3f4f6;
24 | font-size: 15px;
25 | line-height: 1.6;
26 | font-weight: 400;
27 | max-width: 90%;
28 | overflow-y: auto;
29 | max-height: 100%;
30 | }
31 |
32 | .cookie-consent-blocked-content h3 {
33 | font-weight: 600;
34 | margin-bottom: 12px;
35 | color: white;
36 | }
37 |
38 | .cookie-consent-blocked-content p {
39 | margin-bottom: 12px;
40 | opacity: 0.9;
41 | }
42 |
43 | /* Direct content in wrapper */
44 | .cookie-consent-wrapper-content {
45 | position: absolute;
46 | top: 0;
47 | left: 0;
48 | width: 100%;
49 | height: 100%;
50 | display: flex;
51 | flex-direction: column;
52 | align-items: center;
53 | justify-content: center;
54 | text-align: center;
55 | padding: 24px;
56 | color: #f3f4f6;
57 | font-size: 15px;
58 | line-height: 1.6;
59 | z-index: 100;
60 | }
61 |
62 | /* Dark theme support */
63 | .dark .cookie-consent-blocked-iframe {
64 | color: #e5e7eb;
65 | }
66 |
67 | /* Style for the wrapper */
68 | [data-cookie-consent-placeholder="true"] {
69 | display: flex !important;
70 | visibility: visible !important;
71 | opacity: 1 !important;
72 | }
73 |
74 | /* Hide the blocked iframe */
75 | iframe[data-cookie-blocked="true"] {
76 | opacity: 0 !important;
77 | pointer-events: none !important;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/types/types.ts:
--------------------------------------------------------------------------------
1 | export interface CookieConsenterClassNames {
2 | acceptButton?: string;
3 | declineButton?: string;
4 | manageButton?: string;
5 | privacyPolicyLink?: string;
6 | modalContainer?: string;
7 | modalContent?: string;
8 | modalTitle?: string;
9 | modalMessage?: string;
10 | popupContainer?: string;
11 | popupContent?: string;
12 | popupTitle?: string;
13 | popupMessage?: string;
14 | bannerContainer?: string;
15 | bannerContent?: string;
16 | bannerTitle?: string;
17 | bannerMessage?: string;
18 | floatingButton?: string;
19 | floatingButtonCloseButton?: string;
20 | manageCancelButton?: string;
21 | manageSaveButton?: string;
22 | manageCookieContainer?: string;
23 | manageCookieTitle?: string;
24 | manageCookieMessage?: string;
25 | manageCookieCategory?: string;
26 | manageCookieCategoryTitle?: string;
27 | manageCookieCategorySubtitle?: string;
28 | manageCookieStatusText?: string;
29 | manageCookieToggle?: string;
30 | manageCookieToggleChecked?: string;
31 | poweredByLink?: string;
32 | }
33 |
34 | export interface CookieCategories {
35 | Analytics: boolean;
36 | Social: boolean;
37 | Advertising: boolean;
38 | }
39 |
40 | export interface ConsentStatus {
41 | consented: boolean;
42 | timestamp: string;
43 | }
44 |
45 | export interface DetailedCookieConsent {
46 | Analytics: ConsentStatus;
47 | Social: ConsentStatus;
48 | Advertising: ConsentStatus;
49 | }
50 |
51 | export type TranslationKey =
52 | /**
53 | * Text for the accept button
54 | * @default 'Accept'
55 | */
56 | | "buttonText"
57 | /**
58 | * Text for the decline button
59 | * @default 'Decline'
60 | */
61 | | "declineButtonText"
62 | /**
63 | * Text for the manage cookies button
64 | * @default 'Manage Cookies'
65 | */
66 | | "manageButtonText"
67 | /**
68 | * Text for the privacy policy link
69 | * @default 'Privacy Policy'
70 | */
71 | | "privacyPolicyText"
72 | /**
73 | * Optional title for the cookie consent
74 | * @default 'We use cookies'
75 | */
76 | | "title"
77 | /**
78 | * The message to display in the cookie consent banner
79 | * @default 'This website uses cookies to enhance your experience.'
80 | */
81 | | "message"
82 | /**
83 | * The message to display in the manage cookies view
84 | * @default 'Cookie Preferences'
85 | */
86 | | "manageTitle"
87 | /**
88 | * The message to display in the manage cookies view
89 | * @default 'Manage your cookie preferences below. Essential cookies are always enabled as they are necessary for the website to function properly.'
90 | */
91 | | "manageMessage"
92 | /**
93 | * Title for essential cookies in manage cookies view
94 | * @default 'Essential'
95 | */
96 | | "manageEssentialTitle"
97 | /**
98 | * Subtitle for essential cookies in manage cookies view
99 | * @default 'Required for the website to function properly'
100 | */
101 | | "manageEssentialSubtitle"
102 | /**
103 | * Status for essential cookies in manage cookies view
104 | * @default 'Status: Always enabled'
105 | */
106 | | "manageEssentialStatus"
107 | /**
108 | * Status for cookies that are always enabled
109 | * @default 'Always On'
110 | */
111 | | "manageEssentialStatusButtonText"
112 | /**
113 | * Title for analytics cookies in manage cookies view
114 | * @default 'Analytics'
115 | */
116 | | "manageAnalyticsTitle"
117 | /**
118 | * Subtitle for analytics cookies in manage cookies view
119 | * @default 'Help us understand how visitors interact with our website'
120 | */
121 | | "manageAnalyticsSubtitle"
122 | /**
123 | * Title for social cookies in manage cookies view
124 | * @default 'Social'
125 | */
126 | | "manageSocialTitle"
127 | /**
128 | * Subtitle for social cookies in manage cookies view
129 | * @default 'Enable social media features and sharing'
130 | */
131 | | "manageSocialSubtitle"
132 | /**
133 | * Title for advertising cookies in manage cookies view
134 | * @default 'Advertising'
135 | */
136 | | "manageAdvertTitle"
137 | /**
138 | * Subtitle for advertising cookies in manage cookies view
139 | * @default 'Personalize advertisements and measure their performance'
140 | */
141 | | "manageAdvertSubtitle"
142 | /**
143 | * Status text for cookies (after they have been declined or approved) in maange cookies view
144 | * @default 'Status: {{status}} on {{date}}'
145 | */
146 | | "manageCookiesStatus"
147 | /**
148 | * Status text for consented cookies in manage cookies view
149 | * @default 'Consented'
150 | */
151 | | "manageCookiesStatusConsented"
152 | /**
153 | * Status text for declined cookies in manage cookies view
154 | * @default 'Declined'
155 | */
156 | | "manageCookiesStatusDeclined"
157 | /**
158 | * Text for cancel button in manage cookies view
159 | * @default 'Cancel'
160 | */
161 | | "manageCancelButtonText"
162 | /**
163 | * Text for save button in manage cookies view
164 | * @default 'Save Preferences'
165 | */
166 | | "manageSaveButtonText";
167 |
168 | export type FullTranslationObject = Record;
169 |
170 | export type TranslationObject = Partial;
171 |
172 | export type TranslationFunction = (
173 | ...args: [key: K, options?: O] | [key: K, defaultValue: any, options?: O]
174 | ) => any;
175 |
176 | export interface CookieConsenterProps {
177 | /**
178 | * Whether to show the manage cookies button
179 | * @default false
180 | */
181 | showManageButton?: boolean;
182 |
183 | /**
184 | * Whether to enable the floating cookie button that appears after consent is closed
185 | * @default false
186 | */
187 | enableFloatingButton?: boolean;
188 |
189 | /**
190 | * URL for the privacy policy
191 | * If not provided, privacy policy link won't be shown
192 | */
193 | privacyPolicyUrl?: string;
194 |
195 | /**
196 | * Name of the cookie to store the consent
197 | * @default 'cookie-consent'
198 | */
199 | cookieKey?: string;
200 |
201 | /**
202 | * Number of days until the cookie expires
203 | * @default 365
204 | */
205 | cookieExpiration?: number;
206 |
207 | /**
208 | * Display type of the consent UI
209 | * @default 'banner'
210 | */
211 | displayType?: "banner" | "popup" | "modal";
212 |
213 | /**
214 | * Position of the banner
215 | * @default 'bottom'
216 | */
217 | position?: "top" | "bottom";
218 |
219 | /**
220 | * Theme of the banner
221 | * @default 'light'
222 | */
223 | theme?: "light" | "dark";
224 |
225 | /**
226 | * Custom class names for the cookie consent UI
227 | */
228 | classNames?: CookieConsenterClassNames;
229 |
230 | /**
231 | * Initial cookie category preferences
232 | * @default { Analytics: false, Social: false, Advertising: false }
233 | */
234 | initialPreferences?: CookieCategories;
235 |
236 | /**
237 | * Detailed consent information including timestamps
238 | */
239 | detailedConsent?: DetailedCookieConsent | null;
240 |
241 | /**
242 | * Callback function when cookies are accepted
243 | */
244 | onAccept?: () => void;
245 |
246 | /**
247 | * Callback function when cookies are declined
248 | */
249 | onDecline?: () => void;
250 |
251 | /**
252 | * Callback function when manage cookies is clicked or preferences are saved
253 | * If categories are provided, it means preferences were saved
254 | */
255 | onManage?: (categories?: CookieCategories) => void;
256 |
257 | /**
258 | * Whether the manage cookies view is currently shown
259 | * @default false
260 | */
261 | isManaging?: boolean;
262 |
263 | /**
264 | * Whether the consent UI is exiting
265 | */
266 | isExiting?: boolean;
267 |
268 | /**
269 | * Whether the consent UI is entering
270 | */
271 | isEntering?: boolean;
272 |
273 | /**
274 | * Whether to disable automatic blocking of common analytics and tracking scripts
275 | * When false (default), this will block common third-party tracking scripts and requests until consent is given
276 | * @default false
277 | */
278 | disableAutomaticBlocking?: boolean;
279 |
280 | /**
281 | * Custom domains to block in addition to the default list
282 | * Only applies when automatic blocking is enabled
283 | */
284 | blockedDomains?: string[];
285 |
286 | /**
287 | * Whether to force show the cookie consent banner
288 | * @default false
289 | */
290 | forceShow?: boolean;
291 |
292 | /**
293 | * Optional identifier for cookie kit analytics
294 | * When provided, generates a unique session ID for tracking consent events
295 | */
296 | cookieKitId?: string;
297 | }
298 |
--------------------------------------------------------------------------------
/src/utils/cn.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/cookie-blocking/content-blocker.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Handles blocking of tracking scripts and iframes, replacing them with placeholders
3 | */
4 |
5 | /**
6 | * Applies common styling to the wrapper element
7 | * @param wrapper The wrapper element to style
8 | * @param width Optional width to apply
9 | * @param height Optional height to apply
10 | */
11 | const applyWrapperStyles = (
12 | wrapper: HTMLElement,
13 | width: string = "100%",
14 | height: string = "315px"
15 | ): void => {
16 | wrapper.style.position = "relative";
17 | wrapper.style.width = width;
18 | wrapper.style.height = height;
19 | wrapper.style.display = "flex";
20 | wrapper.style.flexDirection = "column";
21 | wrapper.style.alignItems = "center";
22 | wrapper.style.justifyContent = "center";
23 | wrapper.style.backgroundColor = "rgba(31, 41, 55, 0.95)";
24 | wrapper.style.borderRadius = "6px";
25 | wrapper.style.border = "1px solid #4b5563";
26 | wrapper.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.15)";
27 | wrapper.style.overflow = "hidden";
28 | wrapper.style.backdropFilter = "blur(4px)";
29 | wrapper.style.textAlign = "center";
30 | wrapper.style.color = "#f3f4f6";
31 | wrapper.style.fontSize = "14px";
32 | wrapper.style.lineHeight = "1.4";
33 | wrapper.style.fontFamily =
34 | "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
35 | };
36 |
37 | /**
38 | * Creates the content HTML for the blocked content placeholder
39 | * @param placeholderId The unique ID for the placeholder
40 | * @returns HTML string for the placeholder content
41 | */
42 | const createPlaceholderContent = (placeholderId: string): string => {
43 | return `
44 |
45 |
🔒
46 |
Content Blocked
47 |
This content requires cookies that are currently blocked by your privacy settings. This embedded content may track your activity.
48 |
After accepting cookies, please refresh the page to view this content.
49 |
50 | Manage Cookie Settings
51 |
52 |
53 | `;
54 | };
55 |
56 | /**
57 | * Adds event listeners to the settings button
58 | * @param placeholderId The unique ID for the placeholder
59 | */
60 | const addSettingsButtonListeners = (placeholderId: string): void => {
61 | const settingsButton = document.getElementById(
62 | `cookie-settings-${placeholderId}`
63 | );
64 | if (settingsButton) {
65 | settingsButton.addEventListener("mouseover", () => {
66 | settingsButton.style.backgroundColor = "#2563eb";
67 | });
68 | settingsButton.addEventListener("mouseout", () => {
69 | settingsButton.style.backgroundColor = "#3b82f6";
70 | });
71 | settingsButton.addEventListener("click", () => {
72 | // Try to show cookie settings
73 | window.dispatchEvent(new CustomEvent("show-cookie-consent"));
74 | });
75 | }
76 | };
77 |
78 | /**
79 | * Positions an iframe absolutely within its parent to prevent layout disruption
80 | * @param iframe The iframe element to position
81 | */
82 | const positionIframeAbsolutely = (iframe: HTMLIFrameElement): void => {
83 | iframe.style.position = "absolute";
84 | iframe.style.top = "0";
85 | iframe.style.left = "0";
86 | iframe.style.width = "1px";
87 | iframe.style.height = "1px";
88 | iframe.style.opacity = "0";
89 | iframe.style.pointerEvents = "none";
90 | iframe.style.visibility = "hidden";
91 | iframe.style.zIndex = "-1";
92 | };
93 |
94 | /**
95 | * Creates a placeholder for blocked content
96 | * @param iframe The iframe element to block
97 | * @param originalSrc The original source URL of the iframe
98 | * @returns The created wrapper element containing the placeholder
99 | */
100 | export const createContentPlaceholder = (
101 | iframe: HTMLIFrameElement,
102 | originalSrc: string
103 | ): HTMLDivElement => {
104 | // Create a unique ID for the placeholder
105 | const placeholderId = `cookie-blocked-content-${Math.random()
106 | .toString(36)
107 | .substring(2, 11)}`;
108 |
109 | // Get the iframe's parent element
110 | const parentElement = iframe.parentElement;
111 | if (!parentElement) {
112 | throw new Error("Iframe has no parent element");
113 | }
114 |
115 | // Make the iframe invisible but keep it in place
116 | iframe.setAttribute("data-cookie-blocked", "true");
117 | iframe.setAttribute("data-original-src", originalSrc);
118 | iframe.src = "about:blank";
119 |
120 | // Create a wrapper div with position relative
121 | const wrapper = document.createElement("div");
122 | applyWrapperStyles(
123 | wrapper,
124 | iframe.style.width || "100%",
125 | iframe.style.height || "315px"
126 | );
127 |
128 | // Add content directly to the wrapper
129 | wrapper.innerHTML = createPlaceholderContent(placeholderId);
130 |
131 | // Create the placeholder for tracking purposes only
132 | const placeholderElement = document.createElement("div");
133 | placeholderElement.id = placeholderId;
134 | placeholderElement.className = "cookie-consent-blocked-iframe";
135 | placeholderElement.setAttribute("data-cookie-consent-placeholder", "true");
136 | placeholderElement.setAttribute("data-blocked-src", originalSrc);
137 | placeholderElement.style.display = "none"; // Hide the placeholder as we're not using it for display
138 |
139 | // Insert the wrapper right before the iframe
140 | parentElement.insertBefore(wrapper, iframe);
141 |
142 | // Position the iframe absolutely within the wrapper to prevent layout disruption
143 | positionIframeAbsolutely(iframe);
144 |
145 | // Move the iframe inside the wrapper
146 | wrapper.appendChild(iframe);
147 |
148 | // Add the hidden placeholder to the wrapper for tracking
149 | wrapper.appendChild(placeholderElement);
150 |
151 | // Add event listener to the button
152 | addSettingsButtonListeners(placeholderId);
153 |
154 | return wrapper;
155 | };
156 |
157 | /**
158 | * Blocks tracking scripts and iframes based on keywords
159 | * @param trackingKeywords Array of keywords to block
160 | * @returns MutationObserver that watches for new elements
161 | */
162 | export const blockTrackingScripts = (
163 | trackingKeywords: string[]
164 | ): MutationObserver => {
165 | // Remove all script tags that match tracking domains
166 | document.querySelectorAll("script").forEach((script) => {
167 | if (
168 | script.src &&
169 | trackingKeywords.some((keyword) => script.src.includes(keyword))
170 | ) {
171 | script.remove();
172 | }
173 | });
174 |
175 | // Also block iframes from tracking domains (especially for YouTube embeds)
176 | document.querySelectorAll("iframe").forEach((iframe) => {
177 | if (
178 | iframe.src &&
179 | trackingKeywords.some((keyword) => iframe.src.includes(keyword))
180 | ) {
181 | createContentPlaceholder(iframe, iframe.src);
182 | }
183 | });
184 |
185 | // Prevent new tracking scripts and iframes from being injected
186 | const observer = new MutationObserver((mutations) => {
187 | mutations.forEach((mutation) => {
188 | mutation.addedNodes.forEach((node) => {
189 | // Handle script tags
190 | if (node instanceof HTMLElement && node.tagName === "SCRIPT") {
191 | const src = node.getAttribute("src");
192 | if (
193 | src &&
194 | trackingKeywords.some((keyword) => src.includes(keyword))
195 | ) {
196 | node.remove();
197 | }
198 | }
199 |
200 | // Handle iframe tags (especially YouTube)
201 | if (node instanceof HTMLElement && node.tagName === "IFRAME") {
202 | const src = node.getAttribute("src");
203 | if (
204 | src &&
205 | trackingKeywords.some((keyword) => src.includes(keyword))
206 | ) {
207 | createContentPlaceholder(node as HTMLIFrameElement, src);
208 | }
209 | }
210 | });
211 | });
212 | });
213 |
214 | observer.observe(document.documentElement, {
215 | childList: true,
216 | subtree: true,
217 | });
218 |
219 | return observer;
220 | };
221 |
222 | /**
223 | * Ensures that all placeholders remain visible and properly styled
224 | */
225 | export const ensurePlaceholdersVisible = (): void => {
226 | const placeholders = document.querySelectorAll(
227 | '[data-cookie-consent-placeholder="true"]'
228 | );
229 |
230 | if (placeholders.length > 0) {
231 | placeholders.forEach((placeholder) => {
232 | // Make sure the placeholder is visible
233 | if (placeholder instanceof HTMLElement) {
234 | placeholder.style.display = "flex";
235 | placeholder.style.visibility = "visible";
236 | placeholder.style.opacity = "1";
237 | placeholder.style.zIndex = "100";
238 |
239 | // Find the parent wrapper
240 | const wrapper = placeholder.parentElement;
241 | if (wrapper) {
242 | // Make sure the wrapper is properly positioned
243 | applyWrapperStyles(wrapper);
244 |
245 | // Check if we already have content in the wrapper
246 | const hasContent =
247 | wrapper.querySelector(".cookie-consent-wrapper-content") !== null ||
248 | wrapper.innerHTML.includes("Content Blocked");
249 |
250 | // If no content exists, add it directly to the wrapper
251 | if (!hasContent) {
252 | const placeholderId =
253 | placeholder.id ||
254 | `cookie-blocked-content-${Math.random()
255 | .toString(36)
256 | .substring(2, 11)}`;
257 |
258 | // Get the blocked source if available
259 | const blockedSrc =
260 | placeholder.getAttribute("data-blocked-src") || "unknown source";
261 |
262 | wrapper.innerHTML = createPlaceholderContent(placeholderId);
263 |
264 | // Re-append the placeholder to the wrapper
265 | wrapper.appendChild(placeholder);
266 |
267 | // Find the iframe inside the wrapper
268 | const iframe = wrapper.querySelector(
269 | "iframe"
270 | ) as HTMLIFrameElement | null;
271 | if (iframe) {
272 | // Position the iframe absolutely to prevent layout disruption
273 | positionIframeAbsolutely(iframe);
274 |
275 | // Make sure it's still using about:blank
276 | if (
277 | iframe.src !== "about:blank" &&
278 | iframe.hasAttribute("data-original-src")
279 | ) {
280 | iframe.src = "about:blank";
281 | }
282 |
283 | // Re-append the iframe to the wrapper
284 | wrapper.appendChild(iframe);
285 | }
286 |
287 | // Add event listener to the button
288 | addSettingsButtonListeners(placeholderId);
289 | }
290 |
291 | // Find the iframe inside the wrapper
292 | const iframe = wrapper.querySelector(
293 | "iframe"
294 | ) as HTMLIFrameElement | null;
295 | if (iframe) {
296 | // Position the iframe absolutely to prevent layout disruption
297 | positionIframeAbsolutely(iframe);
298 |
299 | // Make sure it's still using about:blank
300 | if (
301 | iframe.src !== "about:blank" &&
302 | iframe.hasAttribute("data-original-src")
303 | ) {
304 | iframe.src = "about:blank";
305 | }
306 | }
307 | }
308 | }
309 | });
310 | }
311 | };
312 |
--------------------------------------------------------------------------------
/src/utils/cookie-blocking/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | blockTrackingRequests,
3 | restoreOriginalRequests,
4 | } from "./request-blocker";
5 | import {
6 | blockTrackingScripts,
7 | ensurePlaceholdersVisible,
8 | createContentPlaceholder,
9 | } from "./content-blocker";
10 |
11 | /**
12 | * Main cookie blocking manager that handles all aspects of cookie blocking
13 | */
14 | export class CookieBlockingManager {
15 | private observerRef: MutationObserver | null = null;
16 | private intervalId: number | null = null;
17 |
18 | /**
19 | * Initializes cookie blocking based on user preferences
20 | * @param blockedHosts Array of hosts to block
21 | * @param blockedKeywords Array of keywords to block in scripts and iframes
22 | */
23 | public initialize(blockedHosts: string[], blockedKeywords: string[]): void {
24 | // Block network requests
25 | if (blockedHosts.length > 0) {
26 | blockTrackingRequests(blockedHosts);
27 | }
28 |
29 | // Block scripts and iframes
30 | if (blockedKeywords.length > 0) {
31 | this.observerRef = blockTrackingScripts(blockedKeywords);
32 |
33 | // Set up periodic check to ensure placeholders remain visible
34 | this.startPlaceholderVisibilityCheck();
35 | }
36 | }
37 |
38 | /**
39 | * Starts a periodic check to ensure placeholders remain visible
40 | */
41 | private startPlaceholderVisibilityCheck(): void {
42 | // Run the check immediately
43 | ensurePlaceholdersVisible();
44 |
45 | // Then set up an interval
46 | this.intervalId = window.setInterval(ensurePlaceholdersVisible, 2000);
47 | }
48 |
49 | /**
50 | * Cleans up all cookie blocking functionality
51 | */
52 | public cleanup(): void {
53 | // Restore original request functions
54 | restoreOriginalRequests();
55 |
56 | // Disconnect observer
57 | if (this.observerRef) {
58 | this.observerRef.disconnect();
59 | this.observerRef = null;
60 | }
61 |
62 | // Clear interval
63 | if (this.intervalId !== null) {
64 | window.clearInterval(this.intervalId);
65 | this.intervalId = null;
66 | }
67 | }
68 | }
69 |
70 | // Export individual functions for direct use
71 | export {
72 | blockTrackingRequests,
73 | restoreOriginalRequests,
74 | blockTrackingScripts,
75 | ensurePlaceholdersVisible,
76 | createContentPlaceholder,
77 | };
78 |
--------------------------------------------------------------------------------
/src/utils/cookie-blocking/request-blocker.ts:
--------------------------------------------------------------------------------
1 | // Store original functions
2 | let originalXhrOpen: typeof XMLHttpRequest.prototype.open | null = null;
3 | let originalFetch: typeof window.fetch | null = null;
4 |
5 | /**
6 | * Blocks network requests to specified domains by overriding XMLHttpRequest and fetch
7 | * @param blockedHosts Array of domain strings to block
8 | */
9 | export const blockTrackingRequests = (blockedHosts: string[]) => {
10 | // Store original functions if not already stored
11 | if (!originalXhrOpen) {
12 | originalXhrOpen = XMLHttpRequest.prototype.open;
13 | }
14 | if (!originalFetch) {
15 | originalFetch = window.fetch;
16 | }
17 |
18 | // Override XMLHttpRequest to block requests to tracking domains
19 | XMLHttpRequest.prototype.open = function (method: string, url: string | URL) {
20 | const urlString = url.toString();
21 | if (blockedHosts.some((host) => urlString.includes(host))) {
22 | console.debug(`[CookieKit] Blocked XMLHttpRequest to: ${urlString}`);
23 | throw new Error(`Request to ${urlString} blocked by consent settings`);
24 | }
25 | return originalXhrOpen!.apply(this, arguments as any);
26 | };
27 |
28 | // Override fetch API to block tracking requests
29 | window.fetch = function (url: RequestInfo | URL, options?: RequestInit) {
30 | const urlString = url.toString();
31 | if (
32 | typeof urlString === "string" &&
33 | blockedHosts.some((host) => urlString.includes(host))
34 | ) {
35 | console.debug(`[CookieKit] Blocked fetch request to: ${urlString}`);
36 | return Promise.resolve(
37 | new Response(null, {
38 | status: 403,
39 | statusText: "Blocked by consent settings",
40 | })
41 | );
42 | }
43 | return originalFetch!.apply(this, arguments as any);
44 | };
45 | };
46 |
47 | /**
48 | * Restores the original XMLHttpRequest and fetch implementations
49 | */
50 | export const restoreOriginalRequests = () => {
51 | if (originalXhrOpen) {
52 | XMLHttpRequest.prototype.open = originalXhrOpen;
53 | }
54 | if (originalFetch) {
55 | window.fetch = originalFetch;
56 | }
57 | };
58 |
--------------------------------------------------------------------------------
/src/utils/cookie-utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Sets a cookie with the specified name, value, and expiration days
3 | * @param name The name of the cookie
4 | * @param value The value to store in the cookie
5 | * @param days Number of days until the cookie expires
6 | */
7 | export const setCookie = (name: string, value: string, days: number): void => {
8 | if (typeof window === "undefined") return; // SSR-safe
9 | const date = new Date();
10 | date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
11 | document.cookie = `${name}=${value};expires=${date.toUTCString()};path=/;SameSite=Lax`;
12 | };
13 |
14 | /**
15 | * Gets a cookie value by name
16 | * @param name The name of the cookie to retrieve
17 | * @returns The cookie value or null if not found
18 | */
19 | export const getCookie = (name: string): string | null => {
20 | if (typeof window === "undefined") return null; // SSR-safe
21 | const value = `; ${document.cookie}`;
22 | const parts = value.split(`; ${name}=`);
23 | if (parts.length === 2) {
24 | return parts.pop()?.split(";").shift() || null;
25 | }
26 | return null;
27 | };
28 |
29 | /**
30 | * Deletes a cookie by setting its expiration to a past date
31 | * @param name The name of the cookie to delete
32 | */
33 | export const deleteCookie = (name: string): void => {
34 | if (typeof window === "undefined") return; // SSR-safe
35 | document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
36 | };
37 |
38 | /**
39 | * Checks if a cookie exists
40 | * @param name The name of the cookie to check
41 | * @returns True if the cookie exists, false otherwise
42 | */
43 | export const cookieExists = (name: string): boolean => {
44 | if (typeof window === "undefined") return false; // SSR-safe
45 | return getCookie(name) !== null;
46 | };
47 |
48 | /**
49 | * Gets all cookies as an object
50 | * @returns An object with cookie names as keys and values as values
51 | */
52 | export const getAllCookies = (): Record => {
53 | if (typeof window === "undefined") return {}; // SSR-safe
54 | const cookies: Record = {};
55 | document.cookie.split(";").forEach((cookie) => {
56 | const [name, value] = cookie.trim().split("=");
57 | if (name && value) {
58 | cookies[name] = value;
59 | }
60 | });
61 | return cookies;
62 | };
63 |
64 | /**
65 | * Clears all cookies from the current domain
66 | */
67 | export const clearAllCookies = (): void => {
68 | if (typeof window === "undefined") return; // SSR-safe
69 | const cookies = getAllCookies();
70 | Object.keys(cookies).forEach((name) => {
71 | deleteCookie(name);
72 | });
73 | };
74 |
--------------------------------------------------------------------------------
/src/utils/session-utils.ts:
--------------------------------------------------------------------------------
1 | import { timezoneToCountryCodeMap } from "./timeZoneMap";
2 | import type { CookieCategories } from "../types/types";
3 |
4 | /**
5 | * Generates a random string of specified length
6 | * @param length The length of the random string
7 | * @returns A random string
8 | */
9 | export const generateRandomString = (length: number): string => {
10 | const characters =
11 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
12 | let result = "";
13 | const charactersLength = characters.length;
14 | for (let i = 0; i < length; i++) {
15 | result += characters.charAt(Math.floor(Math.random() * charactersLength));
16 | }
17 | return result;
18 | };
19 |
20 | /**
21 | * Generates a unique ID based on various entropy sources
22 | * @returns A promise that resolves to a unique ID string
23 | */
24 | export const generateUniqueId = async (): Promise => {
25 | // Get high-precision timestamp
26 | const timestamp = performance.now().toString();
27 |
28 | // Generate random values using crypto API if available
29 | let randomValues = "";
30 | if (window.crypto && window.crypto.getRandomValues) {
31 | const array = new Uint32Array(2);
32 | window.crypto.getRandomValues(array);
33 | randomValues = Array.from(array)
34 | .map((n) => n.toString(36))
35 | .join("");
36 | } else {
37 | randomValues = Math.random().toString(36).substring(2);
38 | }
39 |
40 | // Get some browser-specific info without being too invasive
41 | const browserInfo = [
42 | window.screen.width,
43 | window.screen.height,
44 | navigator.language,
45 | // Use hash of user agent to add entropy without storing the full string
46 | await crypto.subtle
47 | .digest("SHA-256", new TextEncoder().encode(navigator.userAgent))
48 | .then((buf) =>
49 | Array.from(new Uint8Array(buf))
50 | .slice(0, 4)
51 | .map((b) => b.toString(16))
52 | .join("")
53 | ),
54 | ].join("_");
55 |
56 | // Combine all sources of entropy
57 | const combinedString = `${timestamp}_${randomValues}_${browserInfo}`;
58 |
59 | // Hash the combined string for privacy
60 | const hashBuffer = await crypto.subtle.digest(
61 | "SHA-256",
62 | new TextEncoder().encode(combinedString)
63 | );
64 | const hashArray = Array.from(new Uint8Array(hashBuffer));
65 | const hashHex = hashArray
66 | .map((b) => b.toString(16).padStart(2, "0"))
67 | .join("");
68 |
69 | return hashHex.slice(0, 16); // Return first 16 characters of hash
70 | };
71 |
72 | /**
73 | * Generates a session ID for analytics
74 | * @param kitId The cookie kit ID
75 | * @returns A promise that resolves to a session ID string
76 | */
77 | export const generateSessionId = async (kitId: string): Promise => {
78 | const timestamp = new Date().getTime();
79 | const uniqueId = await generateUniqueId();
80 | const randomPart = generateRandomString(8);
81 | return `${kitId}_${timestamp}_${uniqueId}_${randomPart}`;
82 | };
83 |
84 | /**
85 | * Resolves a country code from a timezone
86 | * @param timeZone The timezone to resolve
87 | * @returns The country code or "Unknown" if not found
88 | */
89 | export const resolveCountryFromTimezone = (timeZone: string): string => {
90 | const entry = timezoneToCountryCodeMap[timeZone]?.a
91 | ? timezoneToCountryCodeMap[timezoneToCountryCodeMap[timeZone].a]
92 | : timezoneToCountryCodeMap[timeZone];
93 |
94 | return entry?.c?.[0] ?? "Unknown";
95 | };
96 |
97 | /**
98 | * Posts session data to analytics
99 | * @param kitId The cookie kit ID
100 | * @param sessionId The session ID
101 | * @param action The action performed (e.g., "accept", "decline")
102 | * @param preferences The cookie preferences
103 | * @param userId Optional user ID
104 | */
105 | export const postSessionToAnalytics = async (
106 | kitId: string,
107 | sessionId: string,
108 | action?: string,
109 | preferences?: CookieCategories,
110 | userId?: string
111 | ): Promise => {
112 | try {
113 | const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
114 | const country = resolveCountryFromTimezone(timeZone);
115 | const domain = window.location.hostname;
116 |
117 | const response = await fetch("https://cookiekit.io/api/consents", {
118 | method: "POST",
119 | headers: {
120 | "Content-Type": "application/json",
121 | },
122 | body: JSON.stringify({
123 | website_id: kitId,
124 | session_id: sessionId,
125 | user_id: userId,
126 | analytics: preferences?.Analytics ?? false,
127 | social: preferences?.Social ?? false,
128 | advertising: preferences?.Advertising ?? false,
129 | consent_method: action || "init",
130 | consent_version: "1.0",
131 | user_agent: navigator.userAgent,
132 | location: country,
133 | anonymised_ip: "0.0.0.0",
134 | domain: domain,
135 | }),
136 | });
137 |
138 | if (!response.ok) {
139 | console.warn("Failed to post consent to analytics:", response.statusText);
140 | }
141 | } catch (error) {
142 | console.warn("Error posting consent to analytics:", error);
143 | }
144 | };
145 |
--------------------------------------------------------------------------------
/src/utils/timeZoneMap.ts:
--------------------------------------------------------------------------------
1 | interface TimezoneEntry {
2 | u?: number; // Offset in minutes
3 | a?: string; // Alias timezone
4 | c?: string[]; // List of country codes
5 | r?: number; // Region-specific info, if needed
6 | d?: number;
7 | }
8 |
9 | // Timezone to country code map
10 | type TimezoneToCountryCodeMap = {
11 | [key: string]: TimezoneEntry;
12 | };
13 |
14 | export const timezoneToCountryCodeMap: TimezoneToCountryCodeMap = {
15 | "Africa/Abidjan": {
16 | u: 0,
17 | c: ["CI", "BF", "GH", "GM", "GN", "ML", "MR", "SH", "SL", "SN", "TG"],
18 | },
19 | "Africa/Accra": {
20 | a: "Africa/Abidjan",
21 | c: ["GH"],
22 | r: 1,
23 | },
24 | "Africa/Addis_Ababa": {
25 | a: "Africa/Nairobi",
26 | c: ["ET"],
27 | r: 1,
28 | },
29 | "Africa/Algiers": {
30 | u: 60,
31 | c: ["DZ"],
32 | },
33 | "Africa/Asmara": {
34 | a: "Africa/Nairobi",
35 | c: ["ER"],
36 | r: 1,
37 | },
38 | "Africa/Asmera": {
39 | a: "Africa/Nairobi",
40 | c: ["ER"],
41 | r: 1,
42 | },
43 | "Africa/Bamako": {
44 | a: "Africa/Abidjan",
45 | c: ["ML"],
46 | r: 1,
47 | },
48 | "Africa/Bangui": {
49 | a: "Africa/Lagos",
50 | c: ["CF"],
51 | r: 1,
52 | },
53 | "Africa/Banjul": {
54 | a: "Africa/Abidjan",
55 | c: ["GM"],
56 | r: 1,
57 | },
58 | "Africa/Bissau": {
59 | u: 0,
60 | c: ["GW"],
61 | },
62 | "Africa/Blantyre": {
63 | a: "Africa/Maputo",
64 | c: ["MW"],
65 | r: 1,
66 | },
67 | "Africa/Brazzaville": {
68 | a: "Africa/Lagos",
69 | c: ["CG"],
70 | r: 1,
71 | },
72 | "Africa/Bujumbura": {
73 | a: "Africa/Maputo",
74 | c: ["BI"],
75 | r: 1,
76 | },
77 | "Africa/Cairo": {
78 | u: 120,
79 | c: ["EG"],
80 | },
81 | "Africa/Casablanca": {
82 | u: 60,
83 | d: 0,
84 | c: ["MA"],
85 | },
86 | "Africa/Ceuta": {
87 | u: 60,
88 | d: 120,
89 | c: ["ES"],
90 | },
91 | "Africa/Conakry": {
92 | a: "Africa/Abidjan",
93 | c: ["GN"],
94 | r: 1,
95 | },
96 | "Africa/Dakar": {
97 | a: "Africa/Abidjan",
98 | c: ["SN"],
99 | r: 1,
100 | },
101 | "Africa/Dar_es_Salaam": {
102 | a: "Africa/Nairobi",
103 | c: ["TZ"],
104 | r: 1,
105 | },
106 | "Africa/Djibouti": {
107 | a: "Africa/Nairobi",
108 | c: ["DJ"],
109 | r: 1,
110 | },
111 | "Africa/Douala": {
112 | a: "Africa/Lagos",
113 | c: ["CM"],
114 | r: 1,
115 | },
116 | "Africa/El_Aaiun": {
117 | u: 60,
118 | d: 0,
119 | c: ["EH"],
120 | },
121 | "Africa/Freetown": {
122 | a: "Africa/Abidjan",
123 | c: ["SL"],
124 | r: 1,
125 | },
126 | "Africa/Gaborone": {
127 | a: "Africa/Maputo",
128 | c: ["BW"],
129 | r: 1,
130 | },
131 | "Africa/Harare": {
132 | a: "Africa/Maputo",
133 | c: ["ZW"],
134 | r: 1,
135 | },
136 | "Africa/Johannesburg": {
137 | u: 120,
138 | c: ["ZA", "LS", "SZ"],
139 | },
140 | "Africa/Juba": {
141 | u: 120,
142 | c: ["SS"],
143 | },
144 | "Africa/Kampala": {
145 | a: "Africa/Nairobi",
146 | c: ["UG"],
147 | r: 1,
148 | },
149 | "Africa/Khartoum": {
150 | u: 120,
151 | c: ["SD"],
152 | },
153 | "Africa/Kigali": {
154 | a: "Africa/Maputo",
155 | c: ["RW"],
156 | r: 1,
157 | },
158 | "Africa/Kinshasa": {
159 | a: "Africa/Lagos",
160 | c: ["CD"],
161 | r: 1,
162 | },
163 | "Africa/Lagos": {
164 | u: 60,
165 | c: ["NG", "AO", "BJ", "CD", "CF", "CG", "CM", "GA", "GQ", "NE"],
166 | },
167 | "Africa/Libreville": {
168 | a: "Africa/Lagos",
169 | c: ["GA"],
170 | r: 1,
171 | },
172 | "Africa/Lome": {
173 | a: "Africa/Abidjan",
174 | c: ["TG"],
175 | r: 1,
176 | },
177 | "Africa/Luanda": {
178 | a: "Africa/Lagos",
179 | c: ["AO"],
180 | r: 1,
181 | },
182 | "Africa/Lubumbashi": {
183 | a: "Africa/Maputo",
184 | c: ["CD"],
185 | r: 1,
186 | },
187 | "Africa/Lusaka": {
188 | a: "Africa/Maputo",
189 | c: ["ZM"],
190 | r: 1,
191 | },
192 | "Africa/Malabo": {
193 | a: "Africa/Lagos",
194 | c: ["GQ"],
195 | r: 1,
196 | },
197 | "Africa/Maputo": {
198 | u: 120,
199 | c: ["MZ", "BI", "BW", "CD", "MW", "RW", "ZM", "ZW"],
200 | },
201 | "Africa/Maseru": {
202 | a: "Africa/Johannesburg",
203 | c: ["LS"],
204 | r: 1,
205 | },
206 | "Africa/Mbabane": {
207 | a: "Africa/Johannesburg",
208 | c: ["SZ"],
209 | r: 1,
210 | },
211 | "Africa/Mogadishu": {
212 | a: "Africa/Nairobi",
213 | c: ["SO"],
214 | r: 1,
215 | },
216 | "Africa/Monrovia": {
217 | u: 0,
218 | c: ["LR"],
219 | },
220 | "Africa/Nairobi": {
221 | u: 180,
222 | c: ["KE", "DJ", "ER", "ET", "KM", "MG", "SO", "TZ", "UG", "YT"],
223 | },
224 | "Africa/Ndjamena": {
225 | u: 60,
226 | c: ["TD"],
227 | },
228 | "Africa/Niamey": {
229 | a: "Africa/Lagos",
230 | c: ["NE"],
231 | r: 1,
232 | },
233 | "Africa/Nouakchott": {
234 | a: "Africa/Abidjan",
235 | c: ["MR"],
236 | r: 1,
237 | },
238 | "Africa/Ouagadougou": {
239 | a: "Africa/Abidjan",
240 | c: ["BF"],
241 | r: 1,
242 | },
243 | "Africa/Porto-Novo": {
244 | a: "Africa/Lagos",
245 | c: ["BJ"],
246 | r: 1,
247 | },
248 | "Africa/Sao_Tome": {
249 | u: 0,
250 | c: ["ST"],
251 | },
252 | "Africa/Timbuktu": {
253 | a: "Africa/Abidjan",
254 | c: ["ML"],
255 | r: 1,
256 | },
257 | "Africa/Tripoli": {
258 | u: 120,
259 | c: ["LY"],
260 | },
261 | "Africa/Tunis": {
262 | u: 60,
263 | c: ["TN"],
264 | },
265 | "Africa/Windhoek": {
266 | u: 120,
267 | c: ["NA"],
268 | },
269 | "America/Adak": {
270 | u: -600,
271 | d: -540,
272 | c: ["US"],
273 | },
274 | "America/Anchorage": {
275 | u: -540,
276 | d: -480,
277 | c: ["US"],
278 | },
279 | "America/Anguilla": {
280 | a: "America/Puerto_Rico",
281 | c: ["AI"],
282 | r: 1,
283 | },
284 | "America/Antigua": {
285 | a: "America/Puerto_Rico",
286 | c: ["AG"],
287 | r: 1,
288 | },
289 | "America/Araguaina": {
290 | u: -180,
291 | c: ["BR"],
292 | },
293 | "America/Argentina/Buenos_Aires": {
294 | u: -180,
295 | c: ["AR"],
296 | },
297 | "America/Argentina/Catamarca": {
298 | u: -180,
299 | c: ["AR"],
300 | },
301 | "America/Argentina/ComodRivadavia": {
302 | a: "America/Argentina/Catamarca",
303 | r: 1,
304 | },
305 | "America/Argentina/Cordoba": {
306 | u: -180,
307 | c: ["AR"],
308 | },
309 | "America/Argentina/Jujuy": {
310 | u: -180,
311 | c: ["AR"],
312 | },
313 | "America/Argentina/La_Rioja": {
314 | u: -180,
315 | c: ["AR"],
316 | },
317 | "America/Argentina/Mendoza": {
318 | u: -180,
319 | c: ["AR"],
320 | },
321 | "America/Argentina/Rio_Gallegos": {
322 | u: -180,
323 | c: ["AR"],
324 | },
325 | "America/Argentina/Salta": {
326 | u: -180,
327 | c: ["AR"],
328 | },
329 | "America/Argentina/San_Juan": {
330 | u: -180,
331 | c: ["AR"],
332 | },
333 | "America/Argentina/San_Luis": {
334 | u: -180,
335 | c: ["AR"],
336 | },
337 | "America/Argentina/Tucuman": {
338 | u: -180,
339 | c: ["AR"],
340 | },
341 | "America/Argentina/Ushuaia": {
342 | u: -180,
343 | c: ["AR"],
344 | },
345 | "America/Aruba": {
346 | a: "America/Puerto_Rico",
347 | c: ["AW"],
348 | r: 1,
349 | },
350 | "America/Asuncion": {
351 | u: -240,
352 | d: -180,
353 | c: ["PY"],
354 | },
355 | "America/Atikokan": {
356 | a: "America/Panama",
357 | c: ["CA"],
358 | r: 1,
359 | },
360 | "America/Atka": {
361 | a: "America/Adak",
362 | r: 1,
363 | },
364 | "America/Bahia": {
365 | u: -180,
366 | c: ["BR"],
367 | },
368 | "America/Bahia_Banderas": {
369 | u: -360,
370 | d: -300,
371 | c: ["MX"],
372 | },
373 | "America/Barbados": {
374 | u: -240,
375 | c: ["BB"],
376 | },
377 | "America/Belem": {
378 | u: -180,
379 | c: ["BR"],
380 | },
381 | "America/Belize": {
382 | u: -360,
383 | c: ["BZ"],
384 | },
385 | "America/Blanc-Sablon": {
386 | a: "America/Puerto_Rico",
387 | c: ["CA"],
388 | r: 1,
389 | },
390 | "America/Boa_Vista": {
391 | u: -240,
392 | c: ["BR"],
393 | },
394 | "America/Bogota": {
395 | u: -300,
396 | c: ["CO"],
397 | },
398 | "America/Boise": {
399 | u: -420,
400 | d: -360,
401 | c: ["US"],
402 | },
403 | "America/Buenos_Aires": {
404 | a: "America/Argentina/Buenos_Aires",
405 | r: 1,
406 | },
407 | "America/Cambridge_Bay": {
408 | u: -420,
409 | d: -360,
410 | c: ["CA"],
411 | },
412 | "America/Campo_Grande": {
413 | u: -240,
414 | c: ["BR"],
415 | },
416 | "America/Cancun": {
417 | u: -300,
418 | c: ["MX"],
419 | },
420 | "America/Caracas": {
421 | u: -240,
422 | c: ["VE"],
423 | },
424 | "America/Catamarca": {
425 | a: "America/Argentina/Catamarca",
426 | r: 1,
427 | },
428 | "America/Cayenne": {
429 | u: -180,
430 | c: ["GF"],
431 | },
432 | "America/Cayman": {
433 | a: "America/Panama",
434 | c: ["KY"],
435 | r: 1,
436 | },
437 | "America/Chicago": {
438 | u: -360,
439 | d: -300,
440 | c: ["US"],
441 | },
442 | "America/Chihuahua": {
443 | u: -420,
444 | d: -360,
445 | c: ["MX"],
446 | },
447 | "America/Coral_Harbour": {
448 | a: "America/Panama",
449 | c: ["CA"],
450 | r: 1,
451 | },
452 | "America/Cordoba": {
453 | a: "America/Argentina/Cordoba",
454 | r: 1,
455 | },
456 | "America/Costa_Rica": {
457 | u: -360,
458 | c: ["CR"],
459 | },
460 | "America/Creston": {
461 | a: "America/Phoenix",
462 | c: ["CA"],
463 | r: 1,
464 | },
465 | "America/Cuiaba": {
466 | u: -240,
467 | c: ["BR"],
468 | },
469 | "America/Curacao": {
470 | a: "America/Puerto_Rico",
471 | c: ["CW"],
472 | r: 1,
473 | },
474 | "America/Danmarkshavn": {
475 | u: 0,
476 | c: ["GL"],
477 | },
478 | "America/Dawson": {
479 | u: -420,
480 | c: ["CA"],
481 | },
482 | "America/Dawson_Creek": {
483 | u: -420,
484 | c: ["CA"],
485 | },
486 | "America/Denver": {
487 | u: -420,
488 | d: -360,
489 | c: ["US"],
490 | },
491 | "America/Detroit": {
492 | u: -300,
493 | d: -240,
494 | c: ["US"],
495 | },
496 | "America/Dominica": {
497 | a: "America/Puerto_Rico",
498 | c: ["DM"],
499 | r: 1,
500 | },
501 | "America/Edmonton": {
502 | u: -420,
503 | d: -360,
504 | c: ["CA"],
505 | },
506 | "America/Eirunepe": {
507 | u: -300,
508 | c: ["BR"],
509 | },
510 | "America/El_Salvador": {
511 | u: -360,
512 | c: ["SV"],
513 | },
514 | "America/Ensenada": {
515 | a: "America/Tijuana",
516 | r: 1,
517 | },
518 | "America/Fort_Nelson": {
519 | u: -420,
520 | c: ["CA"],
521 | },
522 | "America/Fort_Wayne": {
523 | a: "America/Indiana/Indianapolis",
524 | r: 1,
525 | },
526 | "America/Fortaleza": {
527 | u: -180,
528 | c: ["BR"],
529 | },
530 | "America/Glace_Bay": {
531 | u: -240,
532 | d: -180,
533 | c: ["CA"],
534 | },
535 | "America/Godthab": {
536 | a: "America/Nuuk",
537 | r: 1,
538 | },
539 | "America/Goose_Bay": {
540 | u: -240,
541 | d: -180,
542 | c: ["CA"],
543 | },
544 | "America/Grand_Turk": {
545 | u: -300,
546 | d: -240,
547 | c: ["TC"],
548 | },
549 | "America/Grenada": {
550 | a: "America/Puerto_Rico",
551 | c: ["GD"],
552 | r: 1,
553 | },
554 | "America/Guadeloupe": {
555 | a: "America/Puerto_Rico",
556 | c: ["GP"],
557 | r: 1,
558 | },
559 | "America/Guatemala": {
560 | u: -360,
561 | c: ["GT"],
562 | },
563 | "America/Guayaquil": {
564 | u: -300,
565 | c: ["EC"],
566 | },
567 | "America/Guyana": {
568 | u: -240,
569 | c: ["GY"],
570 | },
571 | "America/Halifax": {
572 | u: -240,
573 | d: -180,
574 | c: ["CA"],
575 | },
576 | "America/Havana": {
577 | u: -300,
578 | d: -240,
579 | c: ["CU"],
580 | },
581 | "America/Hermosillo": {
582 | u: -420,
583 | c: ["MX"],
584 | },
585 | "America/Indiana/Indianapolis": {
586 | u: -300,
587 | d: -240,
588 | c: ["US"],
589 | },
590 | "America/Indiana/Knox": {
591 | u: -360,
592 | d: -300,
593 | c: ["US"],
594 | },
595 | "America/Indiana/Marengo": {
596 | u: -300,
597 | d: -240,
598 | c: ["US"],
599 | },
600 | "America/Indiana/Petersburg": {
601 | u: -300,
602 | d: -240,
603 | c: ["US"],
604 | },
605 | "America/Indiana/Tell_City": {
606 | u: -360,
607 | d: -300,
608 | c: ["US"],
609 | },
610 | "America/Indiana/Vevay": {
611 | u: -300,
612 | d: -240,
613 | c: ["US"],
614 | },
615 | "America/Indiana/Vincennes": {
616 | u: -300,
617 | d: -240,
618 | c: ["US"],
619 | },
620 | "America/Indiana/Winamac": {
621 | u: -300,
622 | d: -240,
623 | c: ["US"],
624 | },
625 | "America/Indianapolis": {
626 | a: "America/Indiana/Indianapolis",
627 | r: 1,
628 | },
629 | "America/Inuvik": {
630 | u: -420,
631 | d: -360,
632 | c: ["CA"],
633 | },
634 | "America/Iqaluit": {
635 | u: -300,
636 | d: -240,
637 | c: ["CA"],
638 | },
639 | "America/Jamaica": {
640 | u: -300,
641 | c: ["JM"],
642 | },
643 | "America/Jujuy": {
644 | a: "America/Argentina/Jujuy",
645 | r: 1,
646 | },
647 | "America/Juneau": {
648 | u: -540,
649 | d: -480,
650 | c: ["US"],
651 | },
652 | "America/Kentucky/Louisville": {
653 | u: -300,
654 | d: -240,
655 | c: ["US"],
656 | },
657 | "America/Kentucky/Monticello": {
658 | u: -300,
659 | d: -240,
660 | c: ["US"],
661 | },
662 | "America/Knox_IN": {
663 | a: "America/Indiana/Knox",
664 | r: 1,
665 | },
666 | "America/Kralendijk": {
667 | a: "America/Puerto_Rico",
668 | c: ["BQ"],
669 | r: 1,
670 | },
671 | "America/La_Paz": {
672 | u: -240,
673 | c: ["BO"],
674 | },
675 | "America/Lima": {
676 | u: -300,
677 | c: ["PE"],
678 | },
679 | "America/Los_Angeles": {
680 | u: -480,
681 | d: -420,
682 | c: ["US"],
683 | },
684 | "America/Louisville": {
685 | a: "America/Kentucky/Louisville",
686 | r: 1,
687 | },
688 | "America/Lower_Princes": {
689 | a: "America/Puerto_Rico",
690 | c: ["SX"],
691 | r: 1,
692 | },
693 | "America/Maceio": {
694 | u: -180,
695 | c: ["BR"],
696 | },
697 | "America/Managua": {
698 | u: -360,
699 | c: ["NI"],
700 | },
701 | "America/Manaus": {
702 | u: -240,
703 | c: ["BR"],
704 | },
705 | "America/Marigot": {
706 | a: "America/Puerto_Rico",
707 | c: ["MF"],
708 | r: 1,
709 | },
710 | "America/Martinique": {
711 | u: -240,
712 | c: ["MQ"],
713 | },
714 | "America/Matamoros": {
715 | u: -360,
716 | d: -300,
717 | c: ["MX"],
718 | },
719 | "America/Mazatlan": {
720 | u: -420,
721 | d: -360,
722 | c: ["MX"],
723 | },
724 | "America/Mendoza": {
725 | a: "America/Argentina/Mendoza",
726 | r: 1,
727 | },
728 | "America/Menominee": {
729 | u: -360,
730 | d: -300,
731 | c: ["US"],
732 | },
733 | "America/Merida": {
734 | u: -360,
735 | d: -300,
736 | c: ["MX"],
737 | },
738 | "America/Metlakatla": {
739 | u: -540,
740 | d: -480,
741 | c: ["US"],
742 | },
743 | "America/Mexico_City": {
744 | u: -360,
745 | d: -300,
746 | c: ["MX"],
747 | },
748 | "America/Miquelon": {
749 | u: -180,
750 | d: -120,
751 | c: ["PM"],
752 | },
753 | "America/Moncton": {
754 | u: -240,
755 | d: -180,
756 | c: ["CA"],
757 | },
758 | "America/Monterrey": {
759 | u: -360,
760 | d: -300,
761 | c: ["MX"],
762 | },
763 | "America/Montevideo": {
764 | u: -180,
765 | c: ["UY"],
766 | },
767 | "America/Montreal": {
768 | a: "America/Toronto",
769 | c: ["CA"],
770 | r: 1,
771 | },
772 | "America/Montserrat": {
773 | a: "America/Puerto_Rico",
774 | c: ["MS"],
775 | r: 1,
776 | },
777 | "America/Nassau": {
778 | a: "America/Toronto",
779 | c: ["BS"],
780 | r: 1,
781 | },
782 | "America/New_York": {
783 | u: -300,
784 | d: -240,
785 | c: ["US"],
786 | },
787 | "America/Nipigon": {
788 | u: -300,
789 | d: -240,
790 | c: ["CA"],
791 | },
792 | "America/Nome": {
793 | u: -540,
794 | d: -480,
795 | c: ["US"],
796 | },
797 | "America/Noronha": {
798 | u: -120,
799 | c: ["BR"],
800 | },
801 | "America/North_Dakota/Beulah": {
802 | u: -360,
803 | d: -300,
804 | c: ["US"],
805 | },
806 | "America/North_Dakota/Center": {
807 | u: -360,
808 | d: -300,
809 | c: ["US"],
810 | },
811 | "America/North_Dakota/New_Salem": {
812 | u: -360,
813 | d: -300,
814 | c: ["US"],
815 | },
816 | "America/Nuuk": {
817 | u: -180,
818 | d: -120,
819 | c: ["GL"],
820 | },
821 | "America/Ojinaga": {
822 | u: -420,
823 | d: -360,
824 | c: ["MX"],
825 | },
826 | "America/Panama": {
827 | u: -300,
828 | c: ["PA", "CA", "KY"],
829 | },
830 | "America/Pangnirtung": {
831 | u: -300,
832 | d: -240,
833 | c: ["CA"],
834 | },
835 | "America/Paramaribo": {
836 | u: -180,
837 | c: ["SR"],
838 | },
839 | "America/Phoenix": {
840 | u: -420,
841 | c: ["US", "CA"],
842 | },
843 | "America/Port-au-Prince": {
844 | u: -300,
845 | d: -240,
846 | c: ["HT"],
847 | },
848 | "America/Port_of_Spain": {
849 | a: "America/Puerto_Rico",
850 | c: ["TT"],
851 | r: 1,
852 | },
853 | "America/Porto_Acre": {
854 | a: "America/Rio_Branco",
855 | r: 1,
856 | },
857 | "America/Porto_Velho": {
858 | u: -240,
859 | c: ["BR"],
860 | },
861 | "America/Puerto_Rico": {
862 | u: -240,
863 | c: [
864 | "PR",
865 | "AG",
866 | "CA",
867 | "AI",
868 | "AW",
869 | "BL",
870 | "BQ",
871 | "CW",
872 | "DM",
873 | "GD",
874 | "GP",
875 | "KN",
876 | "LC",
877 | "MF",
878 | "MS",
879 | "SX",
880 | "TT",
881 | "VC",
882 | "VG",
883 | "VI",
884 | ],
885 | },
886 | "America/Punta_Arenas": {
887 | u: -180,
888 | c: ["CL"],
889 | },
890 | "America/Rainy_River": {
891 | u: -360,
892 | d: -300,
893 | c: ["CA"],
894 | },
895 | "America/Rankin_Inlet": {
896 | u: -360,
897 | d: -300,
898 | c: ["CA"],
899 | },
900 | "America/Recife": {
901 | u: -180,
902 | c: ["BR"],
903 | },
904 | "America/Regina": {
905 | u: -360,
906 | c: ["CA"],
907 | },
908 | "America/Resolute": {
909 | u: -360,
910 | d: -300,
911 | c: ["CA"],
912 | },
913 | "America/Rio_Branco": {
914 | u: -300,
915 | c: ["BR"],
916 | },
917 | "America/Rosario": {
918 | a: "America/Argentina/Cordoba",
919 | r: 1,
920 | },
921 | "America/Santa_Isabel": {
922 | a: "America/Tijuana",
923 | r: 1,
924 | },
925 | "America/Santarem": {
926 | u: -180,
927 | c: ["BR"],
928 | },
929 | "America/Santiago": {
930 | u: -240,
931 | d: -180,
932 | c: ["CL"],
933 | },
934 | "America/Santo_Domingo": {
935 | u: -240,
936 | c: ["DO"],
937 | },
938 | "America/Sao_Paulo": {
939 | u: -180,
940 | c: ["BR"],
941 | },
942 | "America/Scoresbysund": {
943 | u: -60,
944 | d: 0,
945 | c: ["GL"],
946 | },
947 | "America/Shiprock": {
948 | a: "America/Denver",
949 | r: 1,
950 | },
951 | "America/Sitka": {
952 | u: -540,
953 | d: -480,
954 | c: ["US"],
955 | },
956 | "America/St_Barthelemy": {
957 | a: "America/Puerto_Rico",
958 | c: ["BL"],
959 | r: 1,
960 | },
961 | "America/St_Johns": {
962 | u: -150,
963 | d: -90,
964 | c: ["CA"],
965 | },
966 | "America/St_Kitts": {
967 | a: "America/Puerto_Rico",
968 | c: ["KN"],
969 | r: 1,
970 | },
971 | "America/St_Lucia": {
972 | a: "America/Puerto_Rico",
973 | c: ["LC"],
974 | r: 1,
975 | },
976 | "America/St_Thomas": {
977 | a: "America/Puerto_Rico",
978 | c: ["VI"],
979 | r: 1,
980 | },
981 | "America/St_Vincent": {
982 | a: "America/Puerto_Rico",
983 | c: ["VC"],
984 | r: 1,
985 | },
986 | "America/Swift_Current": {
987 | u: -360,
988 | c: ["CA"],
989 | },
990 | "America/Tegucigalpa": {
991 | u: -360,
992 | c: ["HN"],
993 | },
994 | "America/Thule": {
995 | u: -240,
996 | d: -180,
997 | c: ["GL"],
998 | },
999 | "America/Thunder_Bay": {
1000 | u: -300,
1001 | d: -240,
1002 | c: ["CA"],
1003 | },
1004 | "America/Tijuana": {
1005 | u: -480,
1006 | d: -420,
1007 | c: ["MX"],
1008 | },
1009 | "America/Toronto": {
1010 | u: -300,
1011 | d: -240,
1012 | c: ["CA", "BS"],
1013 | },
1014 | "America/Tortola": {
1015 | a: "America/Puerto_Rico",
1016 | c: ["VG"],
1017 | r: 1,
1018 | },
1019 | "America/Vancouver": {
1020 | u: -480,
1021 | d: -420,
1022 | c: ["CA"],
1023 | },
1024 | "America/Virgin": {
1025 | a: "America/Puerto_Rico",
1026 | c: ["VI"],
1027 | r: 1,
1028 | },
1029 | "America/Whitehorse": {
1030 | u: -420,
1031 | c: ["CA"],
1032 | },
1033 | "America/Winnipeg": {
1034 | u: -360,
1035 | d: -300,
1036 | c: ["CA"],
1037 | },
1038 | "America/Yakutat": {
1039 | u: -540,
1040 | d: -480,
1041 | c: ["US"],
1042 | },
1043 | "America/Yellowknife": {
1044 | u: -420,
1045 | d: -360,
1046 | c: ["CA"],
1047 | },
1048 | "Antarctica/Casey": {
1049 | u: 660,
1050 | c: ["AQ"],
1051 | },
1052 | "Antarctica/Davis": {
1053 | u: 420,
1054 | c: ["AQ"],
1055 | },
1056 | "Antarctica/DumontDUrville": {
1057 | a: "Pacific/Port_Moresby",
1058 | c: ["AQ"],
1059 | r: 1,
1060 | },
1061 | "Antarctica/Macquarie": {
1062 | u: 600,
1063 | d: 660,
1064 | c: ["AU"],
1065 | },
1066 | "Antarctica/Mawson": {
1067 | u: 300,
1068 | c: ["AQ"],
1069 | },
1070 | "Antarctica/McMurdo": {
1071 | a: "Pacific/Auckland",
1072 | c: ["AQ"],
1073 | r: 1,
1074 | },
1075 | "Antarctica/Palmer": {
1076 | u: -180,
1077 | c: ["AQ"],
1078 | },
1079 | "Antarctica/Rothera": {
1080 | u: -180,
1081 | c: ["AQ"],
1082 | },
1083 | "Antarctica/South_Pole": {
1084 | a: "Pacific/Auckland",
1085 | c: ["AQ"],
1086 | r: 1,
1087 | },
1088 | "Antarctica/Syowa": {
1089 | a: "Asia/Riyadh",
1090 | c: ["AQ"],
1091 | r: 1,
1092 | },
1093 | "Antarctica/Troll": {
1094 | u: 0,
1095 | d: 120,
1096 | c: ["AQ"],
1097 | },
1098 | "Antarctica/Vostok": {
1099 | u: 360,
1100 | c: ["AQ"],
1101 | },
1102 | "Arctic/Longyearbyen": {
1103 | a: "Europe/Oslo",
1104 | c: ["SJ"],
1105 | r: 1,
1106 | },
1107 | "Asia/Aden": {
1108 | a: "Asia/Riyadh",
1109 | c: ["YE"],
1110 | r: 1,
1111 | },
1112 | "Asia/Almaty": {
1113 | u: 360,
1114 | c: ["KZ"],
1115 | },
1116 | "Asia/Amman": {
1117 | u: 120,
1118 | d: 180,
1119 | c: ["JO"],
1120 | },
1121 | "Asia/Anadyr": {
1122 | u: 720,
1123 | c: ["RU"],
1124 | },
1125 | "Asia/Aqtau": {
1126 | u: 300,
1127 | c: ["KZ"],
1128 | },
1129 | "Asia/Aqtobe": {
1130 | u: 300,
1131 | c: ["KZ"],
1132 | },
1133 | "Asia/Ashgabat": {
1134 | u: 300,
1135 | c: ["TM"],
1136 | },
1137 | "Asia/Ashkhabad": {
1138 | a: "Asia/Ashgabat",
1139 | r: 1,
1140 | },
1141 | "Asia/Atyrau": {
1142 | u: 300,
1143 | c: ["KZ"],
1144 | },
1145 | "Asia/Baghdad": {
1146 | u: 180,
1147 | c: ["IQ"],
1148 | },
1149 | "Asia/Bahrain": {
1150 | a: "Asia/Qatar",
1151 | c: ["BH"],
1152 | r: 1,
1153 | },
1154 | "Asia/Baku": {
1155 | u: 240,
1156 | c: ["AZ"],
1157 | },
1158 | "Asia/Bangkok": {
1159 | u: 420,
1160 | c: ["TH", "KH", "LA", "VN"],
1161 | },
1162 | "Asia/Barnaul": {
1163 | u: 420,
1164 | c: ["RU"],
1165 | },
1166 | "Asia/Beirut": {
1167 | u: 120,
1168 | d: 180,
1169 | c: ["LB"],
1170 | },
1171 | "Asia/Bishkek": {
1172 | u: 360,
1173 | c: ["KG"],
1174 | },
1175 | "Asia/Brunei": {
1176 | u: 480,
1177 | c: ["BN"],
1178 | },
1179 | "Asia/Calcutta": {
1180 | a: "Asia/Kolkata",
1181 | r: 1,
1182 | },
1183 | "Asia/Chita": {
1184 | u: 540,
1185 | c: ["RU"],
1186 | },
1187 | "Asia/Choibalsan": {
1188 | u: 480,
1189 | c: ["MN"],
1190 | },
1191 | "Asia/Chongqing": {
1192 | a: "Asia/Shanghai",
1193 | r: 1,
1194 | },
1195 | "Asia/Chungking": {
1196 | a: "Asia/Shanghai",
1197 | r: 1,
1198 | },
1199 | "Asia/Colombo": {
1200 | u: 330,
1201 | c: ["LK"],
1202 | },
1203 | "Asia/Dacca": {
1204 | a: "Asia/Dhaka",
1205 | r: 1,
1206 | },
1207 | "Asia/Damascus": {
1208 | u: 120,
1209 | d: 180,
1210 | c: ["SY"],
1211 | },
1212 | "Asia/Dhaka": {
1213 | u: 360,
1214 | c: ["BD"],
1215 | },
1216 | "Asia/Dili": {
1217 | u: 540,
1218 | c: ["TL"],
1219 | },
1220 | "Asia/Dubai": {
1221 | u: 240,
1222 | c: ["AE", "OM"],
1223 | },
1224 | "Asia/Dushanbe": {
1225 | u: 300,
1226 | c: ["TJ"],
1227 | },
1228 | "Asia/Famagusta": {
1229 | u: 120,
1230 | d: 180,
1231 | c: ["CY"],
1232 | },
1233 | "Asia/Gaza": {
1234 | u: 120,
1235 | d: 180,
1236 | c: ["PS"],
1237 | },
1238 | "Asia/Harbin": {
1239 | a: "Asia/Shanghai",
1240 | r: 1,
1241 | },
1242 | "Asia/Hebron": {
1243 | u: 120,
1244 | d: 180,
1245 | c: ["PS"],
1246 | },
1247 | "Asia/Ho_Chi_Minh": {
1248 | u: 420,
1249 | c: ["VN"],
1250 | },
1251 | "Asia/Hong_Kong": {
1252 | u: 480,
1253 | c: ["HK"],
1254 | },
1255 | "Asia/Hovd": {
1256 | u: 420,
1257 | c: ["MN"],
1258 | },
1259 | "Asia/Irkutsk": {
1260 | u: 480,
1261 | c: ["RU"],
1262 | },
1263 | "Asia/Istanbul": {
1264 | a: "Europe/Istanbul",
1265 | r: 1,
1266 | },
1267 | "Asia/Jakarta": {
1268 | u: 420,
1269 | c: ["ID"],
1270 | },
1271 | "Asia/Jayapura": {
1272 | u: 540,
1273 | c: ["ID"],
1274 | },
1275 | "Asia/Jerusalem": {
1276 | u: 120,
1277 | d: 180,
1278 | c: ["IL"],
1279 | },
1280 | "Asia/Kabul": {
1281 | u: 270,
1282 | c: ["AF"],
1283 | },
1284 | "Asia/Kamchatka": {
1285 | u: 720,
1286 | c: ["RU"],
1287 | },
1288 | "Asia/Karachi": {
1289 | u: 300,
1290 | c: ["PK"],
1291 | },
1292 | "Asia/Kashgar": {
1293 | a: "Asia/Urumqi",
1294 | r: 1,
1295 | },
1296 | "Asia/Kathmandu": {
1297 | u: 345,
1298 | c: ["NP"],
1299 | },
1300 | "Asia/Katmandu": {
1301 | a: "Asia/Kathmandu",
1302 | r: 1,
1303 | },
1304 | "Asia/Khandyga": {
1305 | u: 540,
1306 | c: ["RU"],
1307 | },
1308 | "Asia/Kolkata": {
1309 | u: 330,
1310 | c: ["IN"],
1311 | },
1312 | "Asia/Krasnoyarsk": {
1313 | u: 420,
1314 | c: ["RU"],
1315 | },
1316 | "Asia/Kuala_Lumpur": {
1317 | u: 480,
1318 | c: ["MY"],
1319 | },
1320 | "Asia/Kuching": {
1321 | u: 480,
1322 | c: ["MY"],
1323 | },
1324 | "Asia/Kuwait": {
1325 | a: "Asia/Riyadh",
1326 | c: ["KW"],
1327 | r: 1,
1328 | },
1329 | "Asia/Macao": {
1330 | a: "Asia/Macau",
1331 | r: 1,
1332 | },
1333 | "Asia/Macau": {
1334 | u: 480,
1335 | c: ["MO"],
1336 | },
1337 | "Asia/Magadan": {
1338 | u: 660,
1339 | c: ["RU"],
1340 | },
1341 | "Asia/Makassar": {
1342 | u: 480,
1343 | c: ["ID"],
1344 | },
1345 | "Asia/Manila": {
1346 | u: 480,
1347 | c: ["PH"],
1348 | },
1349 | "Asia/Muscat": {
1350 | a: "Asia/Dubai",
1351 | c: ["OM"],
1352 | r: 1,
1353 | },
1354 | "Asia/Nicosia": {
1355 | u: 120,
1356 | d: 180,
1357 | c: ["CY"],
1358 | },
1359 | "Asia/Novokuznetsk": {
1360 | u: 420,
1361 | c: ["RU"],
1362 | },
1363 | "Asia/Novosibirsk": {
1364 | u: 420,
1365 | c: ["RU"],
1366 | },
1367 | "Asia/Omsk": {
1368 | u: 360,
1369 | c: ["RU"],
1370 | },
1371 | "Asia/Oral": {
1372 | u: 300,
1373 | c: ["KZ"],
1374 | },
1375 | "Asia/Phnom_Penh": {
1376 | a: "Asia/Bangkok",
1377 | c: ["KH"],
1378 | r: 1,
1379 | },
1380 | "Asia/Pontianak": {
1381 | u: 420,
1382 | c: ["ID"],
1383 | },
1384 | "Asia/Pyongyang": {
1385 | u: 540,
1386 | c: ["KP"],
1387 | },
1388 | "Asia/Qatar": {
1389 | u: 180,
1390 | c: ["QA", "BH"],
1391 | },
1392 | "Asia/Qostanay": {
1393 | u: 360,
1394 | c: ["KZ"],
1395 | },
1396 | "Asia/Qyzylorda": {
1397 | u: 300,
1398 | c: ["KZ"],
1399 | },
1400 | "Asia/Rangoon": {
1401 | a: "Asia/Yangon",
1402 | r: 1,
1403 | },
1404 | "Asia/Riyadh": {
1405 | u: 180,
1406 | c: ["SA", "AQ", "KW", "YE"],
1407 | },
1408 | "Asia/Saigon": {
1409 | a: "Asia/Ho_Chi_Minh",
1410 | r: 1,
1411 | },
1412 | "Asia/Sakhalin": {
1413 | u: 660,
1414 | c: ["RU"],
1415 | },
1416 | "Asia/Samarkand": {
1417 | u: 300,
1418 | c: ["UZ"],
1419 | },
1420 | "Asia/Seoul": {
1421 | u: 540,
1422 | c: ["KR"],
1423 | },
1424 | "Asia/Shanghai": {
1425 | u: 480,
1426 | c: ["CN"],
1427 | },
1428 | "Asia/Singapore": {
1429 | u: 480,
1430 | c: ["SG", "MY"],
1431 | },
1432 | "Asia/Srednekolymsk": {
1433 | u: 660,
1434 | c: ["RU"],
1435 | },
1436 | "Asia/Taipei": {
1437 | u: 480,
1438 | c: ["TW"],
1439 | },
1440 | "Asia/Tashkent": {
1441 | u: 300,
1442 | c: ["UZ"],
1443 | },
1444 | "Asia/Tbilisi": {
1445 | u: 240,
1446 | c: ["GE"],
1447 | },
1448 | "Asia/Tehran": {
1449 | u: 210,
1450 | d: 270,
1451 | c: ["IR"],
1452 | },
1453 | "Asia/Tel_Aviv": {
1454 | a: "Asia/Jerusalem",
1455 | r: 1,
1456 | },
1457 | "Asia/Thimbu": {
1458 | a: "Asia/Thimphu",
1459 | r: 1,
1460 | },
1461 | "Asia/Thimphu": {
1462 | u: 360,
1463 | c: ["BT"],
1464 | },
1465 | "Asia/Tokyo": {
1466 | u: 540,
1467 | c: ["JP"],
1468 | },
1469 | "Asia/Tomsk": {
1470 | u: 420,
1471 | c: ["RU"],
1472 | },
1473 | "Asia/Ujung_Pandang": {
1474 | a: "Asia/Makassar",
1475 | r: 1,
1476 | },
1477 | "Asia/Ulaanbaatar": {
1478 | u: 480,
1479 | c: ["MN"],
1480 | },
1481 | "Asia/Ulan_Bator": {
1482 | a: "Asia/Ulaanbaatar",
1483 | r: 1,
1484 | },
1485 | "Asia/Urumqi": {
1486 | u: 360,
1487 | c: ["CN"],
1488 | },
1489 | "Asia/Ust-Nera": {
1490 | u: 600,
1491 | c: ["RU"],
1492 | },
1493 | "Asia/Vientiane": {
1494 | a: "Asia/Bangkok",
1495 | c: ["LA"],
1496 | r: 1,
1497 | },
1498 | "Asia/Vladivostok": {
1499 | u: 600,
1500 | c: ["RU"],
1501 | },
1502 | "Asia/Yakutsk": {
1503 | u: 540,
1504 | c: ["RU"],
1505 | },
1506 | "Asia/Yangon": {
1507 | u: 390,
1508 | c: ["MM"],
1509 | },
1510 | "Asia/Yekaterinburg": {
1511 | u: 300,
1512 | c: ["RU"],
1513 | },
1514 | "Asia/Yerevan": {
1515 | u: 240,
1516 | c: ["AM"],
1517 | },
1518 | "Atlantic/Azores": {
1519 | u: -60,
1520 | d: 0,
1521 | c: ["PT"],
1522 | },
1523 | "Atlantic/Bermuda": {
1524 | u: -240,
1525 | d: -180,
1526 | c: ["BM"],
1527 | },
1528 | "Atlantic/Canary": {
1529 | u: 0,
1530 | d: 60,
1531 | c: ["ES"],
1532 | },
1533 | "Atlantic/Cape_Verde": {
1534 | u: -60,
1535 | c: ["CV"],
1536 | },
1537 | "Atlantic/Faeroe": {
1538 | a: "Atlantic/Faroe",
1539 | r: 1,
1540 | },
1541 | "Atlantic/Faroe": {
1542 | u: 0,
1543 | d: 60,
1544 | c: ["FO"],
1545 | },
1546 | "Atlantic/Jan_Mayen": {
1547 | a: "Europe/Oslo",
1548 | c: ["SJ"],
1549 | r: 1,
1550 | },
1551 | "Atlantic/Madeira": {
1552 | u: 0,
1553 | d: 60,
1554 | c: ["PT"],
1555 | },
1556 | "Atlantic/Reykjavik": {
1557 | u: 0,
1558 | c: ["IS"],
1559 | },
1560 | "Atlantic/South_Georgia": {
1561 | u: -120,
1562 | c: ["GS"],
1563 | },
1564 | "Atlantic/St_Helena": {
1565 | a: "Africa/Abidjan",
1566 | c: ["SH"],
1567 | r: 1,
1568 | },
1569 | "Atlantic/Stanley": {
1570 | u: -180,
1571 | c: ["FK"],
1572 | },
1573 | "Australia/ACT": {
1574 | a: "Australia/Sydney",
1575 | r: 1,
1576 | },
1577 | "Australia/Adelaide": {
1578 | u: 570,
1579 | d: 630,
1580 | c: ["AU"],
1581 | },
1582 | "Australia/Brisbane": {
1583 | u: 600,
1584 | c: ["AU"],
1585 | },
1586 | "Australia/Broken_Hill": {
1587 | u: 570,
1588 | d: 630,
1589 | c: ["AU"],
1590 | },
1591 | "Australia/Canberra": {
1592 | a: "Australia/Sydney",
1593 | r: 1,
1594 | },
1595 | "Australia/Currie": {
1596 | a: "Australia/Hobart",
1597 | r: 1,
1598 | },
1599 | "Australia/Darwin": {
1600 | u: 570,
1601 | c: ["AU"],
1602 | },
1603 | "Australia/Eucla": {
1604 | u: 525,
1605 | c: ["AU"],
1606 | },
1607 | "Australia/Hobart": {
1608 | u: 600,
1609 | d: 660,
1610 | c: ["AU"],
1611 | },
1612 | "Australia/LHI": {
1613 | a: "Australia/Lord_Howe",
1614 | r: 1,
1615 | },
1616 | "Australia/Lindeman": {
1617 | u: 600,
1618 | c: ["AU"],
1619 | },
1620 | "Australia/Lord_Howe": {
1621 | u: 630,
1622 | d: 660,
1623 | c: ["AU"],
1624 | },
1625 | "Australia/Melbourne": {
1626 | u: 600,
1627 | d: 660,
1628 | c: ["AU"],
1629 | },
1630 | "Australia/NSW": {
1631 | a: "Australia/Sydney",
1632 | r: 1,
1633 | },
1634 | "Australia/North": {
1635 | a: "Australia/Darwin",
1636 | r: 1,
1637 | },
1638 | "Australia/Perth": {
1639 | u: 480,
1640 | c: ["AU"],
1641 | },
1642 | "Australia/Queensland": {
1643 | a: "Australia/Brisbane",
1644 | r: 1,
1645 | },
1646 | "Australia/South": {
1647 | a: "Australia/Adelaide",
1648 | r: 1,
1649 | },
1650 | "Australia/Sydney": {
1651 | u: 600,
1652 | d: 660,
1653 | c: ["AU"],
1654 | },
1655 | "Australia/Tasmania": {
1656 | a: "Australia/Hobart",
1657 | r: 1,
1658 | },
1659 | "Australia/Victoria": {
1660 | a: "Australia/Melbourne",
1661 | r: 1,
1662 | },
1663 | "Australia/West": {
1664 | a: "Australia/Perth",
1665 | r: 1,
1666 | },
1667 | "Australia/Yancowinna": {
1668 | a: "Australia/Broken_Hill",
1669 | r: 1,
1670 | },
1671 | "Brazil/Acre": {
1672 | a: "America/Rio_Branco",
1673 | r: 1,
1674 | },
1675 | "Brazil/DeNoronha": {
1676 | a: "America/Noronha",
1677 | r: 1,
1678 | },
1679 | "Brazil/East": {
1680 | a: "America/Sao_Paulo",
1681 | r: 1,
1682 | },
1683 | "Brazil/West": {
1684 | a: "America/Manaus",
1685 | r: 1,
1686 | },
1687 | CET: {
1688 | u: 60,
1689 | d: 120,
1690 | },
1691 | CST6CDT: {
1692 | u: -360,
1693 | d: -300,
1694 | },
1695 | "Canada/Atlantic": {
1696 | a: "America/Halifax",
1697 | r: 1,
1698 | },
1699 | "Canada/Central": {
1700 | a: "America/Winnipeg",
1701 | r: 1,
1702 | },
1703 | "Canada/Eastern": {
1704 | a: "America/Toronto",
1705 | c: ["CA"],
1706 | r: 1,
1707 | },
1708 | "Canada/Mountain": {
1709 | a: "America/Edmonton",
1710 | r: 1,
1711 | },
1712 | "Canada/Newfoundland": {
1713 | a: "America/St_Johns",
1714 | r: 1,
1715 | },
1716 | "Canada/Pacific": {
1717 | a: "America/Vancouver",
1718 | r: 1,
1719 | },
1720 | "Canada/Saskatchewan": {
1721 | a: "America/Regina",
1722 | r: 1,
1723 | },
1724 | "Canada/Yukon": {
1725 | a: "America/Whitehorse",
1726 | r: 1,
1727 | },
1728 | "Chile/Continental": {
1729 | a: "America/Santiago",
1730 | r: 1,
1731 | },
1732 | "Chile/EasterIsland": {
1733 | a: "Pacific/Easter",
1734 | r: 1,
1735 | },
1736 | Cuba: {
1737 | a: "America/Havana",
1738 | r: 1,
1739 | },
1740 | EET: {
1741 | u: 120,
1742 | d: 180,
1743 | },
1744 | EST: {
1745 | u: -300,
1746 | },
1747 | EST5EDT: {
1748 | u: -300,
1749 | d: -240,
1750 | },
1751 | Egypt: {
1752 | a: "Africa/Cairo",
1753 | r: 1,
1754 | },
1755 | Eire: {
1756 | a: "Europe/Dublin",
1757 | r: 1,
1758 | },
1759 | "Etc/GMT": {
1760 | u: 0,
1761 | },
1762 | "Etc/GMT+0": {
1763 | a: "Etc/GMT",
1764 | r: 1,
1765 | },
1766 | "Etc/GMT+1": {
1767 | u: -60,
1768 | },
1769 | "Etc/GMT+10": {
1770 | u: -600,
1771 | },
1772 | "Etc/GMT+11": {
1773 | u: -660,
1774 | },
1775 | "Etc/GMT+12": {
1776 | u: -720,
1777 | },
1778 | "Etc/GMT+2": {
1779 | u: -120,
1780 | },
1781 | "Etc/GMT+3": {
1782 | u: -180,
1783 | },
1784 | "Etc/GMT+4": {
1785 | u: -240,
1786 | },
1787 | "Etc/GMT+5": {
1788 | u: -300,
1789 | },
1790 | "Etc/GMT+6": {
1791 | u: -360,
1792 | },
1793 | "Etc/GMT+7": {
1794 | u: -420,
1795 | },
1796 | "Etc/GMT+8": {
1797 | u: -480,
1798 | },
1799 | "Etc/GMT+9": {
1800 | u: -540,
1801 | },
1802 | "Etc/GMT-0": {
1803 | a: "Etc/GMT",
1804 | r: 1,
1805 | },
1806 | "Etc/GMT-1": {
1807 | u: 60,
1808 | },
1809 | "Etc/GMT-10": {
1810 | u: 600,
1811 | },
1812 | "Etc/GMT-11": {
1813 | u: 660,
1814 | },
1815 | "Etc/GMT-12": {
1816 | u: 720,
1817 | },
1818 | "Etc/GMT-13": {
1819 | u: 780,
1820 | },
1821 | "Etc/GMT-14": {
1822 | u: 840,
1823 | },
1824 | "Etc/GMT-2": {
1825 | u: 120,
1826 | },
1827 | "Etc/GMT-3": {
1828 | u: 180,
1829 | },
1830 | "Etc/GMT-4": {
1831 | u: 240,
1832 | },
1833 | "Etc/GMT-5": {
1834 | u: 300,
1835 | },
1836 | "Etc/GMT-6": {
1837 | u: 360,
1838 | },
1839 | "Etc/GMT-7": {
1840 | u: 420,
1841 | },
1842 | "Etc/GMT-8": {
1843 | u: 480,
1844 | },
1845 | "Etc/GMT-9": {
1846 | u: 540,
1847 | },
1848 | "Etc/GMT0": {
1849 | a: "Etc/GMT",
1850 | r: 1,
1851 | },
1852 | "Etc/Greenwich": {
1853 | a: "Etc/GMT",
1854 | r: 1,
1855 | },
1856 | "Etc/UCT": {
1857 | a: "Etc/UTC",
1858 | r: 1,
1859 | },
1860 | "Etc/UTC": {
1861 | u: 0,
1862 | },
1863 | "Etc/Universal": {
1864 | a: "Etc/UTC",
1865 | r: 1,
1866 | },
1867 | "Etc/Zulu": {
1868 | a: "Etc/UTC",
1869 | r: 1,
1870 | },
1871 | "Europe/Amsterdam": {
1872 | u: 60,
1873 | d: 120,
1874 | c: ["NL"],
1875 | },
1876 | "Europe/Andorra": {
1877 | u: 60,
1878 | d: 120,
1879 | c: ["AD"],
1880 | },
1881 | "Europe/Astrakhan": {
1882 | u: 240,
1883 | c: ["RU"],
1884 | },
1885 | "Europe/Athens": {
1886 | u: 120,
1887 | d: 180,
1888 | c: ["GR"],
1889 | },
1890 | "Europe/Belfast": {
1891 | a: "Europe/London",
1892 | c: ["GB"],
1893 | r: 1,
1894 | },
1895 | "Europe/Belgrade": {
1896 | u: 60,
1897 | d: 120,
1898 | c: ["RS", "BA", "HR", "ME", "MK", "SI"],
1899 | },
1900 | "Europe/Berlin": {
1901 | u: 60,
1902 | d: 120,
1903 | c: ["DE"],
1904 | },
1905 | "Europe/Bratislava": {
1906 | a: "Europe/Prague",
1907 | c: ["SK"],
1908 | r: 1,
1909 | },
1910 | "Europe/Brussels": {
1911 | u: 60,
1912 | d: 120,
1913 | c: ["BE"],
1914 | },
1915 | "Europe/Bucharest": {
1916 | u: 120,
1917 | d: 180,
1918 | c: ["RO"],
1919 | },
1920 | "Europe/Budapest": {
1921 | u: 60,
1922 | d: 120,
1923 | c: ["HU"],
1924 | },
1925 | "Europe/Busingen": {
1926 | a: "Europe/Zurich",
1927 | c: ["DE"],
1928 | r: 1,
1929 | },
1930 | "Europe/Chisinau": {
1931 | u: 120,
1932 | d: 180,
1933 | c: ["MD"],
1934 | },
1935 | "Europe/Copenhagen": {
1936 | u: 60,
1937 | d: 120,
1938 | c: ["DK"],
1939 | },
1940 | "Europe/Dublin": {
1941 | u: 60,
1942 | d: 0,
1943 | c: ["IE"],
1944 | },
1945 | "Europe/Gibraltar": {
1946 | u: 60,
1947 | d: 120,
1948 | c: ["GI"],
1949 | },
1950 | "Europe/Guernsey": {
1951 | a: "Europe/London",
1952 | c: ["GG"],
1953 | r: 1,
1954 | },
1955 | "Europe/Helsinki": {
1956 | u: 120,
1957 | d: 180,
1958 | c: ["FI", "AX"],
1959 | },
1960 | "Europe/Isle_of_Man": {
1961 | a: "Europe/London",
1962 | c: ["IM"],
1963 | r: 1,
1964 | },
1965 | "Europe/Istanbul": {
1966 | u: 180,
1967 | c: ["TR"],
1968 | },
1969 | "Europe/Jersey": {
1970 | a: "Europe/London",
1971 | c: ["JE"],
1972 | r: 1,
1973 | },
1974 | "Europe/Kaliningrad": {
1975 | u: 120,
1976 | c: ["RU"],
1977 | },
1978 | "Europe/Kiev": {
1979 | u: 120,
1980 | d: 180,
1981 | c: ["UA"],
1982 | },
1983 | "Europe/Kirov": {
1984 | u: 180,
1985 | c: ["RU"],
1986 | },
1987 | "Europe/Lisbon": {
1988 | u: 0,
1989 | d: 60,
1990 | c: ["PT"],
1991 | },
1992 | "Europe/Ljubljana": {
1993 | a: "Europe/Belgrade",
1994 | c: ["SI"],
1995 | r: 1,
1996 | },
1997 | "Europe/London": {
1998 | u: 0,
1999 | d: 60,
2000 | c: ["GB", "GG", "IM", "JE"],
2001 | },
2002 | "Europe/Luxembourg": {
2003 | u: 60,
2004 | d: 120,
2005 | c: ["LU"],
2006 | },
2007 | "Europe/Madrid": {
2008 | u: 60,
2009 | d: 120,
2010 | c: ["ES"],
2011 | },
2012 | "Europe/Malta": {
2013 | u: 60,
2014 | d: 120,
2015 | c: ["MT"],
2016 | },
2017 | "Europe/Mariehamn": {
2018 | a: "Europe/Helsinki",
2019 | c: ["AX"],
2020 | r: 1,
2021 | },
2022 | "Europe/Minsk": {
2023 | u: 180,
2024 | c: ["BY"],
2025 | },
2026 | "Europe/Monaco": {
2027 | u: 60,
2028 | d: 120,
2029 | c: ["MC"],
2030 | },
2031 | "Europe/Moscow": {
2032 | u: 180,
2033 | c: ["RU"],
2034 | },
2035 | "Europe/Nicosia": {
2036 | a: "Asia/Nicosia",
2037 | r: 1,
2038 | },
2039 | "Europe/Oslo": {
2040 | u: 60,
2041 | d: 120,
2042 | c: ["NO", "SJ", "BV"],
2043 | },
2044 | "Europe/Paris": {
2045 | u: 60,
2046 | d: 120,
2047 | c: ["FR"],
2048 | },
2049 | "Europe/Podgorica": {
2050 | a: "Europe/Belgrade",
2051 | c: ["ME"],
2052 | r: 1,
2053 | },
2054 | "Europe/Prague": {
2055 | u: 60,
2056 | d: 120,
2057 | c: ["CZ", "SK"],
2058 | },
2059 | "Europe/Riga": {
2060 | u: 120,
2061 | d: 180,
2062 | c: ["LV"],
2063 | },
2064 | "Europe/Rome": {
2065 | u: 60,
2066 | d: 120,
2067 | c: ["IT", "SM", "VA"],
2068 | },
2069 | "Europe/Samara": {
2070 | u: 240,
2071 | c: ["RU"],
2072 | },
2073 | "Europe/San_Marino": {
2074 | a: "Europe/Rome",
2075 | c: ["SM"],
2076 | r: 1,
2077 | },
2078 | "Europe/Sarajevo": {
2079 | a: "Europe/Belgrade",
2080 | c: ["BA"],
2081 | r: 1,
2082 | },
2083 | "Europe/Saratov": {
2084 | u: 240,
2085 | c: ["RU"],
2086 | },
2087 | "Europe/Simferopol": {
2088 | u: 180,
2089 | c: ["RU", "UA"],
2090 | },
2091 | "Europe/Skopje": {
2092 | a: "Europe/Belgrade",
2093 | c: ["MK"],
2094 | r: 1,
2095 | },
2096 | "Europe/Sofia": {
2097 | u: 120,
2098 | d: 180,
2099 | c: ["BG"],
2100 | },
2101 | "Europe/Stockholm": {
2102 | u: 60,
2103 | d: 120,
2104 | c: ["SE"],
2105 | },
2106 | "Europe/Tallinn": {
2107 | u: 120,
2108 | d: 180,
2109 | c: ["EE"],
2110 | },
2111 | "Europe/Tirane": {
2112 | u: 60,
2113 | d: 120,
2114 | c: ["AL"],
2115 | },
2116 | "Europe/Tiraspol": {
2117 | a: "Europe/Chisinau",
2118 | r: 1,
2119 | },
2120 | "Europe/Ulyanovsk": {
2121 | u: 240,
2122 | c: ["RU"],
2123 | },
2124 | "Europe/Uzhgorod": {
2125 | u: 120,
2126 | d: 180,
2127 | c: ["UA"],
2128 | },
2129 | "Europe/Vaduz": {
2130 | a: "Europe/Zurich",
2131 | c: ["LI"],
2132 | r: 1,
2133 | },
2134 | "Europe/Vatican": {
2135 | a: "Europe/Rome",
2136 | c: ["VA"],
2137 | r: 1,
2138 | },
2139 | "Europe/Vienna": {
2140 | u: 60,
2141 | d: 120,
2142 | c: ["AT"],
2143 | },
2144 | "Europe/Vilnius": {
2145 | u: 120,
2146 | d: 180,
2147 | c: ["LT"],
2148 | },
2149 | "Europe/Volgograd": {
2150 | u: 180,
2151 | c: ["RU"],
2152 | },
2153 | "Europe/Warsaw": {
2154 | u: 60,
2155 | d: 120,
2156 | c: ["PL"],
2157 | },
2158 | "Europe/Zagreb": {
2159 | a: "Europe/Belgrade",
2160 | c: ["HR"],
2161 | r: 1,
2162 | },
2163 | "Europe/Zaporozhye": {
2164 | u: 120,
2165 | d: 180,
2166 | c: ["UA"],
2167 | },
2168 | "Europe/Zurich": {
2169 | u: 60,
2170 | d: 120,
2171 | c: ["CH", "DE", "LI"],
2172 | },
2173 | Factory: {
2174 | u: 0,
2175 | },
2176 | GB: {
2177 | a: "Europe/London",
2178 | c: ["GB"],
2179 | r: 1,
2180 | },
2181 | "GB-Eire": {
2182 | a: "Europe/London",
2183 | c: ["GB"],
2184 | r: 1,
2185 | },
2186 | GMT: {
2187 | a: "Etc/GMT",
2188 | r: 1,
2189 | },
2190 | "GMT+0": {
2191 | a: "Etc/GMT",
2192 | r: 1,
2193 | },
2194 | "GMT-0": {
2195 | a: "Etc/GMT",
2196 | r: 1,
2197 | },
2198 | GMT0: {
2199 | a: "Etc/GMT",
2200 | r: 1,
2201 | },
2202 | Greenwich: {
2203 | a: "Etc/GMT",
2204 | r: 1,
2205 | },
2206 | HST: {
2207 | u: -600,
2208 | },
2209 | Hongkong: {
2210 | a: "Asia/Hong_Kong",
2211 | r: 1,
2212 | },
2213 | Iceland: {
2214 | a: "Atlantic/Reykjavik",
2215 | r: 1,
2216 | },
2217 | "Indian/Antananarivo": {
2218 | a: "Africa/Nairobi",
2219 | c: ["MG"],
2220 | r: 1,
2221 | },
2222 | "Indian/Chagos": {
2223 | u: 360,
2224 | c: ["IO"],
2225 | },
2226 | "Indian/Christmas": {
2227 | u: 420,
2228 | c: ["CX"],
2229 | },
2230 | "Indian/Cocos": {
2231 | u: 390,
2232 | c: ["CC"],
2233 | },
2234 | "Indian/Comoro": {
2235 | a: "Africa/Nairobi",
2236 | c: ["KM"],
2237 | r: 1,
2238 | },
2239 | "Indian/Kerguelen": {
2240 | u: 300,
2241 | c: ["TF", "HM"],
2242 | },
2243 | "Indian/Mahe": {
2244 | u: 240,
2245 | c: ["SC"],
2246 | },
2247 | "Indian/Maldives": {
2248 | u: 300,
2249 | c: ["MV"],
2250 | },
2251 | "Indian/Mauritius": {
2252 | u: 240,
2253 | c: ["MU"],
2254 | },
2255 | "Indian/Mayotte": {
2256 | a: "Africa/Nairobi",
2257 | c: ["YT"],
2258 | r: 1,
2259 | },
2260 | "Indian/Reunion": {
2261 | u: 240,
2262 | c: ["RE", "TF"],
2263 | },
2264 | Iran: {
2265 | a: "Asia/Tehran",
2266 | r: 1,
2267 | },
2268 | Israel: {
2269 | a: "Asia/Jerusalem",
2270 | r: 1,
2271 | },
2272 | Jamaica: {
2273 | a: "America/Jamaica",
2274 | r: 1,
2275 | },
2276 | Japan: {
2277 | a: "Asia/Tokyo",
2278 | r: 1,
2279 | },
2280 | Kwajalein: {
2281 | a: "Pacific/Kwajalein",
2282 | r: 1,
2283 | },
2284 | Libya: {
2285 | a: "Africa/Tripoli",
2286 | r: 1,
2287 | },
2288 | MET: {
2289 | u: 60,
2290 | d: 120,
2291 | },
2292 | MST: {
2293 | u: -420,
2294 | },
2295 | MST7MDT: {
2296 | u: -420,
2297 | d: -360,
2298 | },
2299 | "Mexico/BajaNorte": {
2300 | a: "America/Tijuana",
2301 | r: 1,
2302 | },
2303 | "Mexico/BajaSur": {
2304 | a: "America/Mazatlan",
2305 | r: 1,
2306 | },
2307 | "Mexico/General": {
2308 | a: "America/Mexico_City",
2309 | r: 1,
2310 | },
2311 | NZ: {
2312 | a: "Pacific/Auckland",
2313 | c: ["NZ"],
2314 | r: 1,
2315 | },
2316 | "NZ-CHAT": {
2317 | a: "Pacific/Chatham",
2318 | r: 1,
2319 | },
2320 | Navajo: {
2321 | a: "America/Denver",
2322 | r: 1,
2323 | },
2324 | PRC: {
2325 | a: "Asia/Shanghai",
2326 | r: 1,
2327 | },
2328 | PST8PDT: {
2329 | u: -480,
2330 | d: -420,
2331 | },
2332 | "Pacific/Apia": {
2333 | u: 780,
2334 | c: ["WS"],
2335 | },
2336 | "Pacific/Auckland": {
2337 | u: 720,
2338 | d: 780,
2339 | c: ["NZ", "AQ"],
2340 | },
2341 | "Pacific/Bougainville": {
2342 | u: 660,
2343 | c: ["PG"],
2344 | },
2345 | "Pacific/Chatham": {
2346 | u: 765,
2347 | d: 825,
2348 | c: ["NZ"],
2349 | },
2350 | "Pacific/Chuuk": {
2351 | u: 600,
2352 | c: ["FM"],
2353 | },
2354 | "Pacific/Easter": {
2355 | u: -360,
2356 | d: -300,
2357 | c: ["CL"],
2358 | },
2359 | "Pacific/Efate": {
2360 | u: 660,
2361 | c: ["VU"],
2362 | },
2363 | "Pacific/Enderbury": {
2364 | a: "Pacific/Kanton",
2365 | r: 1,
2366 | },
2367 | "Pacific/Fakaofo": {
2368 | u: 780,
2369 | c: ["TK"],
2370 | },
2371 | "Pacific/Fiji": {
2372 | u: 720,
2373 | d: 780,
2374 | c: ["FJ"],
2375 | },
2376 | "Pacific/Funafuti": {
2377 | u: 720,
2378 | c: ["TV"],
2379 | },
2380 | "Pacific/Galapagos": {
2381 | u: -360,
2382 | c: ["EC"],
2383 | },
2384 | "Pacific/Gambier": {
2385 | u: -540,
2386 | c: ["PF"],
2387 | },
2388 | "Pacific/Guadalcanal": {
2389 | u: 660,
2390 | c: ["SB"],
2391 | },
2392 | "Pacific/Guam": {
2393 | u: 600,
2394 | c: ["GU", "MP"],
2395 | },
2396 | "Pacific/Honolulu": {
2397 | u: -600,
2398 | c: ["US", "UM"],
2399 | },
2400 | "Pacific/Johnston": {
2401 | a: "Pacific/Honolulu",
2402 | c: ["UM"],
2403 | r: 1,
2404 | },
2405 | "Pacific/Kanton": {
2406 | u: 780,
2407 | c: ["KI"],
2408 | },
2409 | "Pacific/Kiritimati": {
2410 | u: 840,
2411 | c: ["KI"],
2412 | },
2413 | "Pacific/Kosrae": {
2414 | u: 660,
2415 | c: ["FM"],
2416 | },
2417 | "Pacific/Kwajalein": {
2418 | u: 720,
2419 | c: ["MH"],
2420 | },
2421 | "Pacific/Majuro": {
2422 | u: 720,
2423 | c: ["MH"],
2424 | },
2425 | "Pacific/Marquesas": {
2426 | u: -510,
2427 | c: ["PF"],
2428 | },
2429 | "Pacific/Midway": {
2430 | a: "Pacific/Pago_Pago",
2431 | c: ["UM"],
2432 | r: 1,
2433 | },
2434 | "Pacific/Nauru": {
2435 | u: 720,
2436 | c: ["NR"],
2437 | },
2438 | "Pacific/Niue": {
2439 | u: -660,
2440 | c: ["NU"],
2441 | },
2442 | "Pacific/Norfolk": {
2443 | u: 660,
2444 | d: 720,
2445 | c: ["NF"],
2446 | },
2447 | "Pacific/Noumea": {
2448 | u: 660,
2449 | c: ["NC"],
2450 | },
2451 | "Pacific/Pago_Pago": {
2452 | u: -660,
2453 | c: ["AS", "UM"],
2454 | },
2455 | "Pacific/Palau": {
2456 | u: 540,
2457 | c: ["PW"],
2458 | },
2459 | "Pacific/Pitcairn": {
2460 | u: -480,
2461 | c: ["PN"],
2462 | },
2463 | "Pacific/Pohnpei": {
2464 | u: 660,
2465 | c: ["FM"],
2466 | },
2467 | "Pacific/Ponape": {
2468 | a: "Pacific/Pohnpei",
2469 | r: 1,
2470 | },
2471 | "Pacific/Port_Moresby": {
2472 | u: 600,
2473 | c: ["PG", "AQ"],
2474 | },
2475 | "Pacific/Rarotonga": {
2476 | u: -600,
2477 | c: ["CK"],
2478 | },
2479 | "Pacific/Saipan": {
2480 | a: "Pacific/Guam",
2481 | c: ["MP"],
2482 | r: 1,
2483 | },
2484 | "Pacific/Samoa": {
2485 | a: "Pacific/Pago_Pago",
2486 | c: ["WS"],
2487 | r: 1,
2488 | },
2489 | "Pacific/Tahiti": {
2490 | u: -600,
2491 | c: ["PF"],
2492 | },
2493 | "Pacific/Tarawa": {
2494 | u: 720,
2495 | c: ["KI"],
2496 | },
2497 | "Pacific/Tongatapu": {
2498 | u: 780,
2499 | c: ["TO"],
2500 | },
2501 | "Pacific/Truk": {
2502 | a: "Pacific/Chuuk",
2503 | r: 1,
2504 | },
2505 | "Pacific/Wake": {
2506 | u: 720,
2507 | c: ["UM"],
2508 | },
2509 | "Pacific/Wallis": {
2510 | u: 720,
2511 | c: ["WF"],
2512 | },
2513 | "Pacific/Yap": {
2514 | a: "Pacific/Chuuk",
2515 | r: 1,
2516 | },
2517 | Poland: {
2518 | a: "Europe/Warsaw",
2519 | r: 1,
2520 | },
2521 | Portugal: {
2522 | a: "Europe/Lisbon",
2523 | r: 1,
2524 | },
2525 | ROC: {
2526 | a: "Asia/Taipei",
2527 | r: 1,
2528 | },
2529 | ROK: {
2530 | a: "Asia/Seoul",
2531 | r: 1,
2532 | },
2533 | Singapore: {
2534 | a: "Asia/Singapore",
2535 | c: ["SG"],
2536 | r: 1,
2537 | },
2538 | Turkey: {
2539 | a: "Europe/Istanbul",
2540 | r: 1,
2541 | },
2542 | UCT: {
2543 | a: "Etc/UTC",
2544 | r: 1,
2545 | },
2546 | "US/Alaska": {
2547 | a: "America/Anchorage",
2548 | r: 1,
2549 | },
2550 | "US/Aleutian": {
2551 | a: "America/Adak",
2552 | r: 1,
2553 | },
2554 | "US/Arizona": {
2555 | a: "America/Phoenix",
2556 | c: ["US"],
2557 | r: 1,
2558 | },
2559 | "US/Central": {
2560 | a: "America/Chicago",
2561 | r: 1,
2562 | },
2563 | "US/East-Indiana": {
2564 | a: "America/Indiana/Indianapolis",
2565 | r: 1,
2566 | },
2567 | "US/Eastern": {
2568 | a: "America/New_York",
2569 | r: 1,
2570 | },
2571 | "US/Hawaii": {
2572 | a: "Pacific/Honolulu",
2573 | c: ["US"],
2574 | r: 1,
2575 | },
2576 | "US/Indiana-Starke": {
2577 | a: "America/Indiana/Knox",
2578 | r: 1,
2579 | },
2580 | "US/Michigan": {
2581 | a: "America/Detroit",
2582 | r: 1,
2583 | },
2584 | "US/Mountain": {
2585 | a: "America/Denver",
2586 | r: 1,
2587 | },
2588 | "US/Pacific": {
2589 | a: "America/Los_Angeles",
2590 | r: 1,
2591 | },
2592 | "US/Samoa": {
2593 | a: "Pacific/Pago_Pago",
2594 | c: ["WS"],
2595 | r: 1,
2596 | },
2597 | UTC: {
2598 | a: "Etc/UTC",
2599 | r: 1,
2600 | },
2601 | Universal: {
2602 | a: "Etc/UTC",
2603 | r: 1,
2604 | },
2605 | "W-SU": {
2606 | a: "Europe/Moscow",
2607 | r: 1,
2608 | },
2609 | WET: {
2610 | u: 0,
2611 | d: 60,
2612 | },
2613 | Zulu: {
2614 | a: "Etc/UTC",
2615 | r: 1,
2616 | },
2617 | };
2618 |
2619 | export const timezoneCountryMap: Record = {
2620 | AD: "Andorra",
2621 | AE: "United Arab Emirates",
2622 | AF: "Afghanistan",
2623 | AG: "Antigua and Barbuda",
2624 | AI: "Anguilla",
2625 | AL: "Albania",
2626 | AM: "Armenia",
2627 | AO: "Angola",
2628 | AQ: "Antarctica",
2629 | AR: "Argentina",
2630 | AS: "American Samoa",
2631 | AT: "Austria",
2632 | AU: "Australia",
2633 | AW: "Aruba",
2634 | AX: "Åland Islands",
2635 | AZ: "Azerbaijan",
2636 | BA: "Bosnia and Herzegovina",
2637 | BB: "Barbados",
2638 | BD: "Bangladesh",
2639 | BE: "Belgium",
2640 | BF: "Burkina Faso",
2641 | BG: "Bulgaria",
2642 | BH: "Bahrain",
2643 | BI: "Burundi",
2644 | BJ: "Benin",
2645 | BL: "Saint Barthélemy",
2646 | BM: "Bermuda",
2647 | BN: "Brunei",
2648 | BO: "Bolivia",
2649 | BQ: "Caribbean Netherlands",
2650 | BR: "Brazil",
2651 | BS: "Bahamas",
2652 | BT: "Bhutan",
2653 | BV: "Bouvet Island",
2654 | BW: "Botswana",
2655 | BY: "Belarus",
2656 | BZ: "Belize",
2657 | CA: "Canada",
2658 | CC: "Cocos Islands",
2659 | CD: "Democratic Republic of the Congo",
2660 | CF: "Central African Republic",
2661 | CG: "Republic of the Congo",
2662 | CH: "Switzerland",
2663 | CI: "Ivory Coast",
2664 | CK: "Cook Islands",
2665 | CL: "Chile",
2666 | CM: "Cameroon",
2667 | CN: "China",
2668 | CO: "Colombia",
2669 | CR: "Costa Rica",
2670 | CU: "Cuba",
2671 | CV: "Cabo Verde",
2672 | CW: "Curaçao",
2673 | CX: "Christmas Island",
2674 | CY: "Cyprus",
2675 | CZ: "Czechia",
2676 | DE: "Germany",
2677 | DJ: "Djibouti",
2678 | DK: "Denmark",
2679 | DM: "Dominica",
2680 | DO: "Dominican Republic",
2681 | DZ: "Algeria",
2682 | EC: "Ecuador",
2683 | EE: "Estonia",
2684 | EG: "Egypt",
2685 | EH: "Western Sahara",
2686 | ER: "Eritrea",
2687 | ES: "Spain",
2688 | ET: "Ethiopia",
2689 | FI: "Finland",
2690 | FJ: "Fiji",
2691 | FK: "Falkland Islands",
2692 | FM: "Micronesia",
2693 | FO: "Faroe Islands",
2694 | FR: "France",
2695 | GA: "Gabon",
2696 | GB: "United Kingdom",
2697 | GD: "Grenada",
2698 | GE: "Georgia",
2699 | GF: "French Guiana",
2700 | GG: "Guernsey",
2701 | GH: "Ghana",
2702 | GI: "Gibraltar",
2703 | GL: "Greenland",
2704 | GM: "Gambia",
2705 | GN: "Guinea",
2706 | GP: "Guadeloupe",
2707 | GQ: "Equatorial Guinea",
2708 | GR: "Greece",
2709 | GS: "South Georgia and the South Sandwich Islands",
2710 | GT: "Guatemala",
2711 | GU: "Guam",
2712 | GW: "Guinea-Bissau",
2713 | GY: "Guyana",
2714 | HK: "Hong Kong",
2715 | HM: "Heard Island and McDonald Islands",
2716 | HN: "Honduras",
2717 | HR: "Croatia",
2718 | HT: "Haiti",
2719 | HU: "Hungary",
2720 | ID: "Indonesia",
2721 | IE: "Ireland",
2722 | IL: "Israel",
2723 | IM: "Isle of Man",
2724 | IN: "India",
2725 | IO: "British Indian Ocean Territory",
2726 | IQ: "Iraq",
2727 | IR: "Iran",
2728 | IS: "Iceland",
2729 | IT: "Italy",
2730 | JE: "Jersey",
2731 | JM: "Jamaica",
2732 | JO: "Jordan",
2733 | JP: "Japan",
2734 | KE: "Kenya",
2735 | KG: "Kyrgyzstan",
2736 | KH: "Cambodia",
2737 | KI: "Kiribati",
2738 | KM: "Comoros",
2739 | KN: "Saint Kitts and Nevis",
2740 | KP: "North Korea",
2741 | KR: "South Korea",
2742 | KW: "Kuwait",
2743 | KY: "Cayman Islands",
2744 | KZ: "Kazakhstan",
2745 | LA: "Laos",
2746 | LB: "Lebanon",
2747 | LC: "Saint Lucia",
2748 | LI: "Liechtenstein",
2749 | LK: "Sri Lanka",
2750 | LR: "Liberia",
2751 | LS: "Lesotho",
2752 | LT: "Lithuania",
2753 | LU: "Luxembourg",
2754 | LV: "Latvia",
2755 | LY: "Libya",
2756 | MA: "Morocco",
2757 | MC: "Monaco",
2758 | MD: "Moldova",
2759 | ME: "Montenegro",
2760 | MF: "Saint Martin",
2761 | MG: "Madagascar",
2762 | MH: "Marshall Islands",
2763 | MK: "North Macedonia",
2764 | ML: "Mali",
2765 | MM: "Myanmar",
2766 | MN: "Mongolia",
2767 | MO: "Macao",
2768 | MP: "Northern Mariana Islands",
2769 | MQ: "Martinique",
2770 | MR: "Mauritania",
2771 | MS: "Montserrat",
2772 | MT: "Malta",
2773 | MU: "Mauritius",
2774 | MV: "Maldives",
2775 | MW: "Malawi",
2776 | MX: "Mexico",
2777 | MY: "Malaysia",
2778 | MZ: "Mozambique",
2779 | NA: "Namibia",
2780 | NC: "New Caledonia",
2781 | NE: "Niger",
2782 | NF: "Norfolk Island",
2783 | NG: "Nigeria",
2784 | NI: "Nicaragua",
2785 | NL: "Netherlands",
2786 | NO: "Norway",
2787 | NP: "Nepal",
2788 | NR: "Nauru",
2789 | NU: "Niue",
2790 | NZ: "New Zealand",
2791 | OM: "Oman",
2792 | PA: "Panama",
2793 | PE: "Peru",
2794 | PF: "French Polynesia",
2795 | PG: "Papua New Guinea",
2796 | PH: "Philippines",
2797 | PK: "Pakistan",
2798 | PL: "Poland",
2799 | PM: "Saint Pierre and Miquelon",
2800 | PN: "Pitcairn",
2801 | PR: "Puerto Rico",
2802 | PS: "Palestine",
2803 | PT: "Portugal",
2804 | PW: "Palau",
2805 | PY: "Paraguay",
2806 | QA: "Qatar",
2807 | RE: "Réunion",
2808 | RO: "Romania",
2809 | RS: "Serbia",
2810 | RU: "Russia",
2811 | RW: "Rwanda",
2812 | SA: "Saudi Arabia",
2813 | SB: "Solomon Islands",
2814 | SC: "Seychelles",
2815 | SD: "Sudan",
2816 | SE: "Sweden",
2817 | SG: "Singapore",
2818 | SH: "Saint Helena, Ascension and Tristan da Cunha",
2819 | SI: "Slovenia",
2820 | SJ: "Svalbard and Jan Mayen",
2821 | SK: "Slovakia",
2822 | SL: "Sierra Leone",
2823 | SM: "San Marino",
2824 | SN: "Senegal",
2825 | SO: "Somalia",
2826 | SR: "Suriname",
2827 | SS: "South Sudan",
2828 | ST: "Sao Tome and Principe",
2829 | SV: "El Salvador",
2830 | SX: "Sint Maarten",
2831 | SY: "Syria",
2832 | SZ: "Eswatini",
2833 | TC: "Turks and Caicos Islands",
2834 | TD: "Chad",
2835 | TF: "French Southern Territories",
2836 | TG: "Togo",
2837 | TH: "Thailand",
2838 | TJ: "Tajikistan",
2839 | TK: "Tokelau",
2840 | TL: "Timor-Leste",
2841 | TM: "Turkmenistan",
2842 | TN: "Tunisia",
2843 | TO: "Tonga",
2844 | TR: "Turkey",
2845 | TT: "Trinidad and Tobago",
2846 | TV: "Tuvalu",
2847 | TW: "Taiwan",
2848 | TZ: "Tanzania",
2849 | UA: "Ukraine",
2850 | UG: "Uganda",
2851 | UM: "United States Minor Outlying Islands",
2852 | US: "United States of America",
2853 | UY: "Uruguay",
2854 | UZ: "Uzbekistan",
2855 | VA: "Holy See",
2856 | VC: "Saint Vincent and the Grenadines",
2857 | VE: "Venezuela",
2858 | VG: "Virgin Islands (UK)",
2859 | VI: "Virgin Islands (US)",
2860 | VN: "Vietnam",
2861 | VU: "Vanuatu",
2862 | WF: "Wallis and Futuna",
2863 | WS: "Samoa",
2864 | YE: "Yemen",
2865 | YT: "Mayotte",
2866 | ZA: "South Africa",
2867 | ZM: "Zambia",
2868 | ZW: "Zimbabwe",
2869 | };
2870 |
--------------------------------------------------------------------------------
/src/utils/tracker-utils.ts:
--------------------------------------------------------------------------------
1 | import { CookieCategories } from "../types/types";
2 | import { trackers } from "./trackers";
3 |
4 | export const getBlockedHosts = (
5 | preferences: CookieCategories | null
6 | ): string[] => {
7 | if (!preferences) {
8 | // If no preferences set, block everything
9 | return Object.values(trackers.categories).flat();
10 | }
11 |
12 | const blockedHosts: string[] = [];
13 |
14 | // Add hosts based on declined categories
15 | if (!preferences.Analytics) {
16 | blockedHosts.push(...trackers.categories.Analytics);
17 | }
18 | if (!preferences.Social) {
19 | blockedHosts.push(...trackers.categories.Social);
20 | }
21 | if (!preferences.Advertising) {
22 | blockedHosts.push(...trackers.categories.Advertising);
23 | }
24 |
25 | return [...new Set(blockedHosts)]; // Remove duplicates
26 | };
27 |
28 | export const getBlockedKeywords = (
29 | preferences: CookieCategories | null
30 | ): string[] => {
31 | if (!preferences) {
32 | // If no preferences set, block everything
33 | return Object.values(trackers.categories)
34 | .flat()
35 | .map((host) => host.replace(/\.[^.]+$/, ""));
36 | }
37 |
38 | const blockedHosts = getBlockedHosts(preferences);
39 | // Convert hosts to keywords by removing the TLD
40 | const keywords = [
41 | ...new Set(blockedHosts.map((host) => host.replace(/\.[^.]+$/, ""))),
42 | ];
43 |
44 | return keywords;
45 | };
46 |
--------------------------------------------------------------------------------
/src/utils/translations.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FullTranslationObject,
3 | TranslationFunction,
4 | TranslationKey,
5 | TranslationObject,
6 | } from "../types/types";
7 |
8 | const DEFAULT_TRANSLATIONS: FullTranslationObject = {
9 | title: "We use cookies",
10 | message:
11 | "We use cookies to ensure the best experience, understand how the site is used, and support basic functionality. You can choose to accept all cookies or adjust your settings.",
12 | buttonText: "Accept",
13 | declineButtonText: "Decline",
14 | manageButtonText: "Manage Cookies",
15 | privacyPolicyText: "Privacy Policy",
16 | manageTitle: "Cookie Preferences",
17 | manageMessage:
18 | "Manage your cookie preferences below. Essential cookies are always enabled as they are necessary for the website to function properly.",
19 | manageEssentialTitle: "Essential",
20 | manageEssentialSubtitle: "Required for the website to function properly",
21 | manageEssentialStatus: "Status: Always enabled",
22 | manageEssentialStatusButtonText: "Always On",
23 | manageAnalyticsTitle: "Analytics",
24 | manageAnalyticsSubtitle:
25 | "Help us understand how visitors interact with our website",
26 | manageSocialTitle: "Social",
27 | manageSocialSubtitle: "Enable social media features and sharing",
28 | manageAdvertTitle: "Advertising",
29 | manageAdvertSubtitle:
30 | "Personalize advertisements and measure their performance",
31 | manageCookiesStatus: "Status: {{status}} on {{date}}",
32 | manageCookiesStatusConsented: "Consented",
33 | manageCookiesStatusDeclined: "Declined",
34 | manageCancelButtonText: "Cancel",
35 | manageSaveButtonText: "Save Preferences",
36 | };
37 |
38 | function getTranslationValue(
39 | tObject: FullTranslationObject,
40 | key: TranslationKey,
41 | params?: Record
42 | ): string {
43 | if (params) {
44 | return Object.keys(params).reduce((acc, param) => {
45 | return acc.replace(
46 | new RegExp(`{{\\s*${param}\\s*}}`, "g"),
47 | params[param]
48 | );
49 | }, tObject[key]);
50 | }
51 |
52 | return tObject[key];
53 | }
54 |
55 | export type TFunction = (
56 | key: TranslationKey,
57 | params?: Record
58 | ) => string;
59 |
60 | export function createTFunction(
61 | translations?: TranslationObject | TranslationFunction,
62 | translationI18NextPrefix?: string
63 | ): TFunction {
64 | if (typeof translations === "function") {
65 | return (key: TranslationKey, params?: Record) => {
66 | const fullKey = `${translationI18NextPrefix || ""}${key}`;
67 | let i18nValue = translations(fullKey, params);
68 | if (i18nValue === fullKey) {
69 | i18nValue = null;
70 | }
71 | return (
72 | i18nValue || getTranslationValue(DEFAULT_TRANSLATIONS, key, params)
73 | );
74 | };
75 | }
76 |
77 | return (key: TranslationKey, params?: Record) => {
78 | return getTranslationValue(
79 | { ...DEFAULT_TRANSLATIONS, ...(translations || {}) },
80 | key,
81 | params
82 | );
83 | };
84 | }
85 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["./src/**/*.{js,ts,jsx,tsx}"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | corePlugins: {
9 | // Disable features we don't use
10 | container: false,
11 | objectFit: false,
12 | objectPosition: false,
13 | overscroll: false,
14 | placeholderColor: false,
15 | placeholderOpacity: false,
16 | tableLayout: false,
17 | },
18 | // Only include the utilities we actually use
19 | };
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": false,
4 | "declaration": true,
5 | "declarationMap": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "inlineSources": false,
9 | "isolatedModules": true,
10 | "moduleResolution": "node",
11 | "noUnusedLocals": false,
12 | "noUnusedParameters": false,
13 | "preserveWatchOutput": true,
14 | "skipLibCheck": true,
15 | "strict": true,
16 | "strictNullChecks": true,
17 | "jsx": "react-jsx",
18 | "lib": ["ESNext", "DOM", "DOM.Iterable"],
19 | "module": "ESNext",
20 | "target": "es6",
21 | "outDir": "./dist",
22 | "rootDir": "./src"
23 | },
24 | "include": ["src/**/*.ts"],
25 | "exclude": ["node_modules", "dist"]
26 | }
27 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import { resolve } from "path";
3 | import react from "@vitejs/plugin-react";
4 | import dts from "vite-plugin-dts";
5 |
6 | export default defineConfig({
7 | plugins: [react(), dts({ include: ["src"] })],
8 | css: {
9 | modules: {
10 | generateScopedName: "[name]__[local]___[hash:base64:5]",
11 | },
12 | },
13 | build: {
14 | lib: {
15 | entry: resolve(__dirname, "src/index.ts"),
16 | name: "react-cookie-consenter",
17 | fileName: (format) => `index.${format === "es" ? "js" : "cjs"}`,
18 | formats: ["es"],
19 | },
20 | rollupOptions: {
21 | external: ["react", "react/jsx-runtime", "react-dom"],
22 | output: {
23 | assetFileNames: "style.css",
24 | manualChunks: undefined,
25 | inlineDynamicImports: true,
26 | minifyInternalExports: true,
27 | },
28 | },
29 | minify: "terser",
30 | terserOptions: {
31 | compress: {
32 | drop_console: true,
33 | drop_debugger: true,
34 | pure_funcs: [
35 | "console.log",
36 | "console.info",
37 | "console.debug",
38 | "console.trace",
39 | ],
40 | },
41 | },
42 | sourcemap: false,
43 | reportCompressedSize: true,
44 | },
45 | });
46 |
--------------------------------------------------------------------------------