From e33c52f7f40f663864be1325e7a62c554c9b8cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1aki=20Amatria=20Barral?= Date: Mon, 9 Dec 2024 18:41:19 +0100 Subject: [PATCH] Web: Add `CookieConsentBanner` --- docusaurus.config.ts | 4 + .../CookieConsentBanner.css | 64 +++++++++ .../CookieConsentBanner.tsx | 136 ++++++++++++++++++ src/theme/Footer/LinkItem/index.tsx | 6 + 4 files changed, 210 insertions(+) create mode 100644 src/components/CookieConsentBanner/CookieConsentBanner.css create mode 100644 src/components/CookieConsentBanner/CookieConsentBanner.tsx diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 2d7b153..7d8d61e 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -126,6 +126,10 @@ const config: Config = { label: 'Privacy policy', href: '/privacy-policy', }, + { + label: 'Manage cookies', + href: '#', + }, ], }, ], diff --git a/src/components/CookieConsentBanner/CookieConsentBanner.css b/src/components/CookieConsentBanner/CookieConsentBanner.css new file mode 100644 index 0000000..334a6b8 --- /dev/null +++ b/src/components/CookieConsentBanner/CookieConsentBanner.css @@ -0,0 +1,64 @@ +:root { + --cookie-consent-banner-container-background-color: #000000ff; + --cookie-consent-banner-heading-color: #eeeeeeff; + --cookie-consent-banner-text-color: #94a1b2; + --cookie-consent-banner-button-accept-color: #0be0a6ff; + --cookie-consent-banner-button-accept-text-color: #000000; + --cookie-consent-banner-button-decline-color: #eeeeeeff; + --cookie-consent-banner-button-decline-text-color: #000000; +} + +.cookie-consent-banner-container { + font-family: inherit; + background: var(--cookie-consent-banner-container-background-color); + color: var(--cookie-consent-banner-text-color); + padding: 25px; + width: calc(100% - 60px); + max-width: 400px; + border-radius: 8px; + box-shadow: 0px 8px 12px rgba(0, 0, 0, 0.45); + position: fixed; + bottom: 30px; + right: 30px; + z-index: 9999; + display: flex; + flex-direction: column; +} + +.cookie-consent-banner-buttons { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 10px; +} + +@media (max-width: 768px) { + .cookie-consent-banner-buttons { + flex-direction: column; + } +} + +.cookie-consent-banner-button { + font-family: inherit; + font-weight: bold; + text-align: center; + user-select: text; + padding: 10px 0px; + border: none; + border-radius: 4px; + flex: 1; +} + +.cookie-consent-banner-button:hover { + cursor: pointer; +} + +.cookie-consent-banner-button-accept { + background: var(--cookie-consent-banner-button-accept-color); + color: var(--cookie-consent-banner-button-accept-text-color); +} + +.cookie-consent-banner-button-decline { + background: var(--cookie-consent-banner-button-decline-color); + color: var(--cookie-consent-banner-button-decline-text-color); +} diff --git a/src/components/CookieConsentBanner/CookieConsentBanner.tsx b/src/components/CookieConsentBanner/CookieConsentBanner.tsx new file mode 100644 index 0000000..f021ee8 --- /dev/null +++ b/src/components/CookieConsentBanner/CookieConsentBanner.tsx @@ -0,0 +1,136 @@ +import React, { useEffect, useState } from 'react'; +import CookieConsent, { getCookieConsentValue, OPTIONS, VISIBLE_OPTIONS } from 'react-cookie-consent'; +import ReactGA from 'react-ga4'; +import Link from '@docusaurus/Link'; +import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; + +import './CookieConsentBanner.css'; + +const CookieConsentBanner = () => { + const cookieName: string = 'CookieConsent'; + const googleAnalyticsTrackingId: string = 'G-178ZNT1Z63'; + // Store the banner state in memory to maintain consistency across route + // changes. For example, if the banner is visible, ensure it remains visible + // when navigating between pages. + const isMinimizedValueInMemoryKey: string = 'CookieConsentIsMinimized'; + + if (!ExecutionEnvironment.canUseDOM) { + // Avoid rendering this component during Server-Side Rendering (SSR) + // to prevent errors caused by accessing the DOM in a non-browser + // environment. + return null; + } + + const getIsMinimizedValueFromMemory = (): undefined | boolean => { + const isMinimizedValueInMemory: null | string = + window.sessionStorage.getItem(isMinimizedValueInMemoryKey); + if (isMinimizedValueInMemory === null) { + return undefined; + } + return JSON.parse(isMinimizedValueInMemory) === true; + }; + const setIsMinimizedValueInMemory = (value: boolean): void => { + window.sessionStorage.setItem(isMinimizedValueInMemoryKey, JSON.stringify(value)); + }; + const initializeGoogleAnalytics = (): void => { + ReactGA.initialize([{ trackingId: googleAnalyticsTrackingId }]); + }; + const resetGoogleAnalytics = (): void => { + ReactGA.reset(); + }; + const getCookieValue = (): undefined | boolean => { + const cookieValue: undefined | string = getCookieConsentValue(cookieName); + if (cookieValue === undefined) { + return undefined; + } + return cookieValue === 'true'; + }; + + const [isMinimized, setIsMinimized] = useState(() => { + const isMinimizedValueInMemory: undefined | boolean = getIsMinimizedValueFromMemory(); + if (isMinimizedValueInMemory === undefined) { + // If `isMinimizedValueInMemory` is undefined, we decide whether to show + // the banner by checking if the value of the cookie is also undefined. If + // the cookie is undefined, it means it is the first time the user visits + // the website; therefore, prompt the banner + return getCookieValue() !== undefined; + } + return isMinimizedValueInMemory; + }); + + const handleAccept = () => { + initializeGoogleAnalytics(); + setIsMinimized(true); + }; + const handleDecline = () => { + resetGoogleAnalytics(); + setIsMinimized(true); + }; + const handleExpand = () => { + setIsMinimized(false); + }; + + useEffect(() => { + setIsMinimizedValueInMemory(isMinimized); + }, [isMinimized]); + useEffect(() => { + if (getCookieValue() === true) { + initializeGoogleAnalytics(); + } + }, []); + + return ( + <> + {!isMinimized && ( + +

+ 🍪 We use cookies! +

+

+ We only use cookies to enhance your experience and improve our site. Is + that okay with you? +

+

+ + Read more + +

+
+ )} + { + // Prevent the default anchor behavior to avoid scrolling the page to + // the top when the link is clicked. + e.preventDefault(); + handleExpand(); + }}>Manage cookies + + ); +}; + +export default CookieConsentBanner; diff --git a/src/theme/Footer/LinkItem/index.tsx b/src/theme/Footer/LinkItem/index.tsx index 3ca37b9..3a29e8a 100644 --- a/src/theme/Footer/LinkItem/index.tsx +++ b/src/theme/Footer/LinkItem/index.tsx @@ -6,6 +6,7 @@ import isInternalUrl from '@docusaurus/isInternalUrl'; import IconExternalLink from '@theme/Icon/ExternalLink'; import type {Props} from '@theme/Footer/LinkItem'; import { Icon } from '@iconify/react'; +import CookieConsentBanner from '@site/src/components/CookieConsentBanner/CookieConsentBanner'; export default function FooterLinkItem({item}: Props): JSX.Element { const {to, href, label, prependBaseUrlToHref, ...props} = item; @@ -13,6 +14,11 @@ export default function FooterLinkItem({item}: Props): JSX.Element { const normalizedHref = useBaseUrl(href, {forcePrependBaseUrl: true}); const icon = props['icon'] as string; + if (label == 'Manage cookies') { + // Render the CookieConsentBanner directly instead of a regular footer link + // to allow interactive cookie management without navigating away. + return ; + } return (