Skip to content

Commit 3f0a2df

Browse files
Cookie Consent Banner (#347)
Co-authored-by: Matt Rubens <[email protected]>
1 parent f4fdd7f commit 3f0a2df

File tree

8 files changed

+391
-4
lines changed

8 files changed

+391
-4
lines changed

.tool-versions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nodejs 20.19.2

package-lock.json

Lines changed: 61 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,16 @@
2323
"@docusaurus/preset-classic": "^3.8.1",
2424
"@easyops-cn/docusaurus-search-local": "^0.48.5",
2525
"@mdx-js/react": "^3.0.0",
26+
"@roo-code/types": "^1.79.0",
2627
"@vscode/codicons": "^0.0.36",
2728
"clsx": "^2.0.0",
2829
"posthog-docusaurus": "^2.0.4",
2930
"prism-react-renderer": "^2.3.0",
3031
"react": "^19.0.0",
32+
"react-cookie-consent": "^9.0.0",
3133
"react-dom": "^19.0.0",
32-
"react-icons": "^5.5.0"
34+
"react-icons": "^5.5.0",
35+
"tldts": "^7.0.14"
3336
},
3437
"devDependencies": {
3538
"@docusaurus/module-type-aliases": "^3.8.1",
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React, { useState, useEffect } from 'react';
2+
import ReactCookieConsent from 'react-cookie-consent';
3+
import { getDomain } from 'tldts';
4+
import { CONSENT_COOKIE_NAME } from '../../constants';
5+
import { dispatchConsentEvent } from '../../lib/analytics/consent-manager';
6+
import styles from './styles.module.css';
7+
8+
export function CookieConsent() {
9+
const [cookieDomain, setCookieDomain] = useState<string | null>(null);
10+
11+
useEffect(() => {
12+
// Get the appropriate domain using tldts
13+
if (typeof window !== 'undefined') {
14+
if(window.location.hostname === 'localhost') {
15+
setCookieDomain('localhost');
16+
return;
17+
} else {
18+
setCookieDomain(getDomain(window.location.hostname));
19+
}
20+
}
21+
}, []);
22+
23+
const handleAccept = () => {
24+
dispatchConsentEvent(true);
25+
};
26+
27+
const handleDecline = () => {
28+
dispatchConsentEvent(false);
29+
};
30+
31+
const extraCookieOptions = cookieDomain
32+
? {
33+
domain: cookieDomain,
34+
}
35+
: {};
36+
37+
return (
38+
<div role="banner" aria-label="Cookie consent banner" aria-live="polite">
39+
<ReactCookieConsent
40+
location="bottom"
41+
buttonText="Accept"
42+
declineButtonText="Decline"
43+
cookieName={CONSENT_COOKIE_NAME}
44+
expires={365}
45+
enableDeclineButton={true}
46+
onAccept={handleAccept}
47+
onDecline={handleDecline}
48+
containerClasses={styles.container}
49+
buttonClasses={styles.acceptButton}
50+
buttonWrapperClasses={styles.buttonWrapper}
51+
declineButtonClasses={styles.declineButton}
52+
extraCookieOptions={extraCookieOptions}
53+
disableStyles={true}
54+
ariaAcceptLabel="Accept cookies"
55+
ariaDeclineLabel="Decline cookies"
56+
>
57+
<div className={styles.content}>
58+
<svg
59+
className={styles.cookieIcon}
60+
xmlns="http://www.w3.org/2000/svg"
61+
width="1.5em"
62+
height="1.5em"
63+
viewBox="0 0 24 24"
64+
fill="none"
65+
stroke="currentColor"
66+
strokeWidth="2"
67+
strokeLinecap="round"
68+
strokeLinejoin="round">
69+
<path d="M12 2a10 10 0 1 0 10 10 4 4 0 0 1-5-5 4 4 0 0 1-5-5" /><path d="M8.5 8.5v.01" /><path d="M16 15.5v.01" /><path d="M12 12v.01" /><path d="M11 17v.01" /><path d="M7 14v.01" />
70+
</svg>
71+
<span>Like most of the internet, we use cookies. Are you OK with that?</span>
72+
</div>
73+
</ReactCookieConsent>
74+
</div>
75+
);
76+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
.container {
2+
position: fixed;
3+
bottom: 0;
4+
left: 8px;
5+
right: 8px;
6+
z-index: 999;
7+
color: white;
8+
font-weight: 600;
9+
border-radius: 8px 8px 0 0;
10+
padding: 16px;
11+
display: flex;
12+
flex-wrap: wrap;
13+
align-items: center;
14+
justify-content: space-between;
15+
gap: 16px;
16+
font-size: 14px;
17+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
18+
}
19+
20+
.container {
21+
background: black !important;
22+
color: white;
23+
border-top-color: #222;
24+
}
25+
26+
/* Dark mode support */
27+
[data-theme='dark'] .container {
28+
background-color: white !important;
29+
color: black;
30+
border-top-color: #e5e5e5;
31+
}
32+
33+
@media (min-width: 768px) {
34+
.container {
35+
padding: 16px 32px;
36+
}
37+
}
38+
39+
.content {
40+
display: flex;
41+
align-items: center;
42+
gap: 8px;
43+
}
44+
45+
.cookieIcon {
46+
display: none;
47+
flex-shrink: 0;
48+
}
49+
50+
@media (min-width: 768px) {
51+
.cookieIcon {
52+
display: block;
53+
}
54+
}
55+
56+
.buttonWrapper {
57+
display: flex;
58+
flex-direction: row-reverse;
59+
align-items: center;
60+
gap: 8px;
61+
}
62+
63+
.acceptButton {
64+
background-color: white;
65+
color: black;
66+
border: 1px solid #262626;
67+
border-radius: 6px;
68+
padding: 8px 16px;
69+
margin-right: 8px;
70+
font-size: 14px;
71+
font-weight: bold;
72+
cursor: pointer;
73+
transition: opacity 0.2s;
74+
}
75+
76+
.acceptButton:hover {
77+
opacity: 0.7;
78+
}
79+
80+
.acceptButton:focus {
81+
outline: none;
82+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
83+
}
84+
85+
[data-theme='dark'] .acceptButton {
86+
background-color: black;
87+
color: white;
88+
border-color: #e5e5e5;
89+
}
90+
91+
.declineButton {
92+
background-color: black;
93+
color: white;
94+
border: 1px solid #262626;
95+
border-radius: 6px;
96+
padding: 8px 16px;
97+
font-size: 14px;
98+
font-weight: bold;
99+
cursor: pointer;
100+
transition: opacity 0.2s;
101+
}
102+
103+
.declineButton:hover {
104+
opacity: 0.7;
105+
}
106+
107+
.declineButton:focus {
108+
outline: none;
109+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
110+
}
111+
112+
[data-theme='dark'] .declineButton {
113+
background-color: white;
114+
color: black;
115+
border-color: #e5e5e5;
116+
}
117+
118+
/* Ensure the banner is above other content */
119+
.container {
120+
animation: slideUp 0.3s ease-out;
121+
}
122+
123+
@keyframes slideUp {
124+
from {
125+
transform: translateY(100%);
126+
}
127+
to {
128+
transform: translateY(0);
129+
}
130+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { hasConsent, onConsentChange } from '../../lib/analytics/consent-manager';
3+
4+
declare global {
5+
interface Window {
6+
posthog?: any;
7+
}
8+
}
9+
10+
export function PostHogProvider({ children }: { children: React.ReactNode }) {
11+
const [isPostHogEnabled, setIsPostHogEnabled] = useState(false);
12+
13+
useEffect(() => {
14+
// Check initial consent status
15+
const consentGiven = hasConsent();
16+
setIsPostHogEnabled(consentGiven);
17+
18+
if (consentGiven) {
19+
enablePostHog();
20+
} else {
21+
disablePostHog();
22+
}
23+
24+
// Listen for consent changes
25+
const cleanup = onConsentChange((granted) => {
26+
setIsPostHogEnabled(granted);
27+
if (granted) {
28+
enablePostHog();
29+
} else {
30+
disablePostHog();
31+
}
32+
});
33+
34+
return cleanup;
35+
}, []);
36+
37+
const enablePostHog = () => {
38+
if (typeof window !== 'undefined' && window.posthog) {
39+
// Re-initialize PostHog if it was previously disabled
40+
window.posthog.opt_in_capturing();
41+
window.posthog.startSessionRecording();
42+
}
43+
};
44+
45+
const disablePostHog = () => {
46+
if (typeof window !== 'undefined' && window.posthog) {
47+
window.posthog.opt_out_capturing();
48+
window.posthog.stopSessionRecording();
49+
}
50+
};
51+
52+
return <>{children}</>;
53+
}

0 commit comments

Comments
 (0)