Add cookies popup (#690)

This commit is contained in:
Brandon Hopkins
2026-04-08 10:25:38 -07:00
committed by GitHub
parent a8b7150ec4
commit 55393f6396
4 changed files with 188 additions and 5 deletions

View File

@@ -1,9 +1,12 @@
import Script from "next/script";
export function MatomoTagManager() {
export function MatomoTagManager({ consentGiven }) {
return (
<Script id="matomo-tag-manager" strategy="afterInteractive">
{`var _mtm = window._mtm = window._mtm || [];
{`var _paq = window._paq = window._paq || [];
_paq.push(['requireCookieConsent']);
${consentGiven ? "_paq.push(['setCookieConsentGiven']);" : ""}
var _mtm = window._mtm = window._mtm || [];
_mtm.push({'mtm.startTime': (new Date().getTime()), 'event': 'mtm.Start'});
(function() {
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];

View File

@@ -0,0 +1,70 @@
import { useCookieConsent } from '@/components/cookie-consent/CookieConsentProvider'
import clsx from 'clsx'
import { useEffect, useState } from 'react'
export function CookieConsent() {
const { showConsent, acceptCookies, declineCookies } = useCookieConsent()
const [visible, setVisible] = useState(false)
useEffect(() => {
if (showConsent) {
// Small delay so the CSS transition plays on mount
const id = setTimeout(() => setVisible(true), 50)
return () => clearTimeout(id)
}
setVisible(false)
}, [showConsent])
if (!showConsent) return null
return (
<div
className={clsx(
'fixed inset-0 z-[999] flex items-center justify-center px-4 transition-opacity duration-300',
visible ? 'opacity-100' : 'opacity-0'
)}
>
{/* Backdrop */}
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
{/* Banner */}
<div className="relative mx-auto max-w-lg rounded-xl border border-neutral-800 bg-black px-8 pb-6 pt-4 shadow-lg">
<h3 className="text-base font-semibold text-white">
We are using cookies
</h3>
<p className="mt-2 text-sm leading-relaxed text-white/70">
We use our own cookies as well as third-party cookies on our websites
to enhance your experience, analyze our traffic, and for security and
marketing. View our{' '}
<a
href="https://netbird.io/privacy"
target="_blank"
rel="noopener noreferrer"
className="text-white underline underline-offset-4 transition-colors hover:text-netbird"
>
Privacy Policy
</a>{' '}
for more information.
</p>
<div className="mt-4 flex items-center justify-between gap-8">
<button
type="button"
onClick={declineCookies}
className="cursor-pointer text-xs text-white/70 underline underline-offset-[6px] transition-colors duration-300 hover:text-netbird"
>
Required only cookies
</button>
<button
type="button"
onClick={acceptCookies}
className="rounded-md bg-netbird px-5 py-2.5 text-sm font-medium text-black transition-colors hover:bg-netbird/90"
>
Accept all cookies
</button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,97 @@
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
const STORAGE_KEY = 'cookie-consent'
const ACCEPT_EXPIRY_DAYS = 90
const DECLINE_EXPIRY_DAYS = 1
const CookieConsentContext = createContext({
isAccepted: false,
showConsent: false,
acceptCookies: () => {},
declineCookies: () => {},
})
function getStoredConsent() {
if (typeof window === 'undefined') return null
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return null
const { value, expires } = JSON.parse(raw)
if (Date.now() > expires) {
localStorage.removeItem(STORAGE_KEY)
return null
}
return value
} catch {
return null
}
}
function storeConsent(value, days) {
try {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({
value,
expires: Date.now() + days * 24 * 60 * 60 * 1000,
})
)
} catch {}
}
export function CookieConsentProvider({ children }) {
const router = useRouter()
const [consent, setConsent] = useState(() => getStoredConsent())
const [showConsent, setShowConsent] = useState(false)
useEffect(() => {
const stored = getStoredConsent()
setConsent(stored)
setShowConsent(stored === null)
}, [])
useEffect(() => {
// Hide banner on privacy-related pages
if (router.pathname.startsWith('/privacy') || router.pathname.startsWith('/terms')) {
setShowConsent(false)
}
}, [router.pathname])
const acceptCookies = useCallback(() => {
storeConsent('accepted', ACCEPT_EXPIRY_DAYS)
setConsent('accepted')
setShowConsent(false)
// Enable Matomo cookies
window._paq = window._paq || []
window._paq.push(['setCookieConsentGiven'])
}, [])
const declineCookies = useCallback(() => {
storeConsent('declined', DECLINE_EXPIRY_DAYS)
setConsent('declined')
setShowConsent(false)
// Tell Matomo to forget consent and delete its cookies
window._paq = window._paq || []
window._paq.push(['forgetCookieConsentGiven'])
}, [])
return (
<CookieConsentContext.Provider
value={{
isAccepted: consent === 'accepted',
showConsent,
acceptCookies,
declineCookies,
}}
>
{children}
</CookieConsentContext.Provider>
)
}
export function useCookieConsent() {
return useContext(CookieConsentContext)
}

View File

@@ -15,6 +15,8 @@ import {dom} from "@fortawesome/fontawesome-svg-core";
import {AnnouncementBannerProvider} from "@/components/announcement-banner/AnnouncementBannerProvider";
import {ImageZoom} from "@/components/ImageZoom";
import {MatomoTagManager} from "@/components/Matomo";
import {CookieConsentProvider, useCookieConsent} from "@/components/cookie-consent/CookieConsentProvider";
import {CookieConsent} from "@/components/cookie-consent/CookieConsent";
function onRouteChange() {
useMobileNavigationStore.getState().close()
@@ -23,12 +25,14 @@ function onRouteChange() {
Router.events.on('routeChangeStart', onRouteChange)
Router.events.on('hashChangeStart', onRouteChange)
export default function App({ Component, pageProps }) {
function AppInner({ Component, pageProps }) {
let router = useRouter()
let tableOfContents = collectHeadings(pageProps.sections)
let tableOfContents = collectHeadings(pageProps.sections)
const { isAccepted } = useCookieConsent()
return (
<>
<MatomoTagManager />
<MatomoTagManager consentGiven={isAccepted} />
<Head>
<style>{dom.css()}</style>
{router.route.startsWith('/ipa') ?
@@ -45,10 +49,19 @@ export default function App({ Component, pageProps }) {
</AnnouncementBannerProvider>
<ToastContainer />
<ImageZoom />
<CookieConsent />
</>
)
}
export default function App(props) {
return (
<CookieConsentProvider>
<AppInner {...props} />
</CookieConsentProvider>
)
}
function collectHeadings(sections, slugify = slugifyWithCounter()) {
let output = []