feat: add cookie consent banner for GDPR compliance
All checks were successful
Build & Push / build-and-push (push) Successful in 2m56s
All checks were successful
Build & Push / build-and-push (push) Successful in 2m56s
Slides up on first visit, remembers choice in localStorage. Accept → grants analytics_storage, Decline → keeps denied. Returning visitors get their previous choice restored silently. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import { routing } from '@/i18n/routing'
|
|||||||
import Nav from '@/components/layout/Nav'
|
import Nav from '@/components/layout/Nav'
|
||||||
import Footer from '@/components/layout/Footer'
|
import Footer from '@/components/layout/Footer'
|
||||||
import GoogleAnalytics from '@/components/analytics/GoogleAnalytics'
|
import GoogleAnalytics from '@/components/analytics/GoogleAnalytics'
|
||||||
|
import CookieConsent from '@/components/analytics/CookieConsent'
|
||||||
import '@/styles/globals.css'
|
import '@/styles/globals.css'
|
||||||
|
|
||||||
const BASE_URL = 'https://letsbe.biz'
|
const BASE_URL = 'https://letsbe.biz'
|
||||||
@@ -107,6 +108,7 @@ export default async function LocaleLayout({ children, params }: Props) {
|
|||||||
<Nav />
|
<Nav />
|
||||||
{children}
|
{children}
|
||||||
<Footer />
|
<Footer />
|
||||||
|
<CookieConsent />
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
79
src/components/analytics/CookieConsent.tsx
Normal file
79
src/components/analytics/CookieConsent.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
const CONSENT_KEY = 'cookie_consent'
|
||||||
|
|
||||||
|
type ConsentState = 'granted' | 'denied'
|
||||||
|
|
||||||
|
function updateConsent(state: ConsentState) {
|
||||||
|
if (typeof window !== 'undefined' && typeof window.gtag === 'function') {
|
||||||
|
window.gtag('consent', 'update', {
|
||||||
|
analytics_storage: state,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CookieConsent() {
|
||||||
|
const [visible, setVisible] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const stored = localStorage.getItem(CONSENT_KEY)
|
||||||
|
if (stored === 'granted' || stored === 'denied') {
|
||||||
|
// Restore previous choice
|
||||||
|
updateConsent(stored)
|
||||||
|
} else {
|
||||||
|
// No choice yet — show banner
|
||||||
|
setVisible(true)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleAccept = () => {
|
||||||
|
localStorage.setItem(CONSENT_KEY, 'granted')
|
||||||
|
updateConsent('granted')
|
||||||
|
setVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDecline = () => {
|
||||||
|
localStorage.setItem(CONSENT_KEY, 'denied')
|
||||||
|
updateConsent('denied')
|
||||||
|
setVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!visible) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-label="Cookie consent"
|
||||||
|
style={{ animation: 'cookie-slide-up 0.5s ease-out' }}
|
||||||
|
className="fixed bottom-0 left-0 right-0 z-50 p-4 sm:p-6"
|
||||||
|
>
|
||||||
|
<div className="max-w-xl mx-auto rounded-2xl bg-surface-high border border-outline-variant/20 shadow-[0_-4px_32px_rgba(25,28,29,0.12)] p-5 sm:p-6">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
|
||||||
|
<p className="text-sm text-outline leading-relaxed flex-1">
|
||||||
|
We use cookies to understand how visitors use our site.
|
||||||
|
No personal data is shared with third parties.
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2.5 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDecline}
|
||||||
|
className="px-4 py-2 rounded-xl text-sm font-medium text-outline transition-colors hover:bg-on-surface/5 cursor-pointer"
|
||||||
|
>
|
||||||
|
Decline
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAccept}
|
||||||
|
className="px-5 py-2 rounded-xl text-sm font-medium text-white transition-all hover:-translate-y-px active:translate-y-0 cursor-pointer shadow-[0_4px_16px_rgba(0,100,148,0.25)]"
|
||||||
|
style={{ background: 'linear-gradient(135deg, #006494, #5BA4D9)' }}
|
||||||
|
>
|
||||||
|
Accept
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -91,6 +91,11 @@
|
|||||||
87.5% { transform: translate(-120px, 120px); }
|
87.5% { transform: translate(-120px, 120px); }
|
||||||
100% { transform: translate(0px, 170px); }
|
100% { transform: translate(0px, 170px); }
|
||||||
}
|
}
|
||||||
|
@keyframes cookie-slide-up {
|
||||||
|
from { transform: translateY(100%); opacity: 0; }
|
||||||
|
to { transform: translateY(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes hero-orbit-b {
|
@keyframes hero-orbit-b {
|
||||||
0% { transform: translate(0px, -130px); }
|
0% { transform: translate(0px, -130px); }
|
||||||
12.5% { transform: translate(-90px, -90px); }
|
12.5% { transform: translate(-90px, -90px); }
|
||||||
|
|||||||
Reference in New Issue
Block a user