├── .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 | [![React Cookie Manager Hero](https://github.com/hypershiphq/react-cookie-manager/blob/main/assets/github-hero-banner.jpg?raw=true)](https://cookiekit.io) 6 | 7 | ![React Cookie Manager](https://github.com/hypershiphq/react-cookie-manager/blob/main/assets/react-cookie-manager.gif?raw=true) 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 | ![React Cookie Manager Styles](https://github.com/hypershiphq/react-cookie-manager/blob/main/assets/banner-styles.jpg?raw=true) 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 |
32 | 33 | React logo 34 | 35 | 🍪 36 |
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 |
572 | {showManageButton && ( 573 | 579 | )} 580 | {privacyPolicyUrl && ( 581 | 587 | {tFunction("privacyPolicyText")} 588 | 589 | )} 590 |
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 | 116 | {/* Chocolate chips */} 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | {/* Main cookie shape with bite mark */} 125 | 140 | 141 | {/* Cookie texture lines */} 142 | 143 | 149 | 155 | 161 | 167 | 168 | 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 |
499 |
500 |
507 | 524 |
525 |
526 |
, 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 | 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 | --------------------------------------------------------------------------------