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 Footer from '@/components/layout/Footer'
|
||||
import GoogleAnalytics from '@/components/analytics/GoogleAnalytics'
|
||||
import CookieConsent from '@/components/analytics/CookieConsent'
|
||||
import '@/styles/globals.css'
|
||||
|
||||
const BASE_URL = 'https://letsbe.biz'
|
||||
@@ -107,6 +108,7 @@ export default async function LocaleLayout({ children, params }: Props) {
|
||||
<Nav />
|
||||
{children}
|
||||
<Footer />
|
||||
<CookieConsent />
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</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); }
|
||||
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 {
|
||||
0% { transform: translate(0px, -130px); }
|
||||
12.5% { transform: translate(-90px, -90px); }
|
||||
|
||||
Reference in New Issue
Block a user