Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/components/Matomo.jsx
Original file line number Diff line number Diff line change
@@ -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];
Expand Down
70 changes: 70 additions & 0 deletions src/components/cookie-consent/CookieConsent.jsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
97 changes: 97 additions & 0 deletions src/components/cookie-consent/CookieConsentProvider.jsx
Original file line number Diff line number Diff line change
@@ -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)
}
19 changes: 16 additions & 3 deletions src/pages/_app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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') ?
Expand All @@ -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 = []

Expand Down
Loading