gdpr-compliance-kit/components/CookieConsent.tsx

276 lines
9.2 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
type ConsentState = {
necessary: boolean;
analytics: boolean;
preferences: boolean;
timestamp: string;
};
const CONSENT_COOKIE_NAME = "gdpr_consent";
const CONSENT_VERSION = "1.0";
export function CookieConsent() {
const [showBanner, setShowBanner] = useState(false);
const [showDetails, setShowDetails] = useState(false);
const [consent, setConsent] = useState<ConsentState>({
necessary: true, // Always required
analytics: false,
preferences: false,
timestamp: "",
});
useEffect(() => {
// Check if consent has already been given
const savedConsent = localStorage.getItem(CONSENT_COOKIE_NAME);
if (savedConsent) {
try {
const parsed = JSON.parse(savedConsent);
setConsent(parsed);
// Apply saved consent settings
applyConsent(parsed);
} catch {
// Invalid consent data, show banner
setShowBanner(true);
}
} else {
// No consent yet, show banner
setShowBanner(true);
}
}, []);
const applyConsent = (consentState: ConsentState) => {
// Enable/disable analytics based on consent
if (consentState.analytics) {
// Enable analytics (e.g., Vercel Analytics, Plausible, etc.)
window.localStorage.setItem("va_disabled", "false");
// If using Google Analytics, you would enable it here
// window.gtag?.('consent', 'update', { analytics_storage: 'granted' });
} else {
// Disable analytics
window.localStorage.setItem("va_disabled", "true");
// window.gtag?.('consent', 'update', { analytics_storage: 'denied' });
}
};
const saveConsent = (consentState: ConsentState) => {
const consentWithTimestamp = {
...consentState,
timestamp: new Date().toISOString(),
version: CONSENT_VERSION,
};
localStorage.setItem(CONSENT_COOKIE_NAME, JSON.stringify(consentWithTimestamp));
setConsent(consentWithTimestamp);
applyConsent(consentWithTimestamp);
setShowBanner(false);
};
const acceptAll = () => {
saveConsent({
necessary: true,
analytics: true,
preferences: true,
timestamp: "",
});
};
const rejectAll = () => {
saveConsent({
necessary: true,
analytics: false,
preferences: false,
timestamp: "",
});
};
const saveCustom = () => {
saveConsent(consent);
};
if (!showBanner) return null;
return (
<div
className="fixed bottom-0 left-0 right-0 z-50 p-4 bg-background/95 backdrop-blur-sm border-t border-border shadow-lg"
role="dialog"
aria-label="Cookie consent"
aria-modal="true"
>
<div className="max-w-4xl mx-auto">
{!showDetails ? (
// Simple banner view
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="flex-1">
<h2 className="text-lg font-semibold mb-1">We value your privacy</h2>
<p className="text-sm text-muted-foreground">
We use cookies to improve your experience and analyze site usage.
You can choose which cookies to accept.{" "}
<Link href="/privacy" className="underline hover:text-foreground">
Learn more
</Link>
</p>
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setShowDetails(true)}
className="px-4 py-2 text-sm border border-border rounded-md hover:bg-accent transition-colors"
>
Customize
</button>
<button
onClick={rejectAll}
className="px-4 py-2 text-sm border border-border rounded-md hover:bg-accent transition-colors"
>
Reject All
</button>
<button
onClick={acceptAll}
className="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
>
Accept All
</button>
</div>
</div>
) : (
// Detailed settings view
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">Cookie Settings</h2>
<button
onClick={() => setShowDetails(false)}
className="text-muted-foreground hover:text-foreground"
aria-label="Close details"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="space-y-4 mb-6">
{/* Necessary Cookies */}
<div className="flex items-start justify-between p-3 bg-accent/50 rounded-lg">
<div className="flex-1">
<div className="flex items-center gap-2">
<h3 className="font-medium">Strictly Necessary</h3>
<span className="text-xs bg-primary/20 text-primary px-2 py-0.5 rounded">Always Active</span>
</div>
<p className="text-sm text-muted-foreground mt-1">
Essential for the website to function properly. These cannot be disabled.
</p>
</div>
<input
type="checkbox"
checked={true}
disabled
className="mt-1 h-4 w-4 rounded border-border"
aria-label="Necessary cookies (always enabled)"
/>
</div>
{/* Analytics Cookies */}
<div className="flex items-start justify-between p-3 border border-border rounded-lg">
<div className="flex-1">
<h3 className="font-medium">Analytics</h3>
<p className="text-sm text-muted-foreground mt-1">
Help us understand how visitors interact with our website by collecting anonymous usage data.
</p>
</div>
<input
type="checkbox"
checked={consent.analytics}
onChange={(e) => setConsent({ ...consent, analytics: e.target.checked })}
className="mt-1 h-4 w-4 rounded border-border cursor-pointer"
aria-label="Enable analytics cookies"
/>
</div>
{/* Preference Cookies */}
<div className="flex items-start justify-between p-3 border border-border rounded-lg">
<div className="flex-1">
<h3 className="font-medium">Preferences</h3>
<p className="text-sm text-muted-foreground mt-1">
Remember your settings and preferences like theme choice and language.
</p>
</div>
<input
type="checkbox"
checked={consent.preferences}
onChange={(e) => setConsent({ ...consent, preferences: e.target.checked })}
className="mt-1 h-4 w-4 rounded border-border cursor-pointer"
aria-label="Enable preference cookies"
/>
</div>
</div>
<div className="flex flex-wrap gap-2 justify-end">
<button
onClick={rejectAll}
className="px-4 py-2 text-sm border border-border rounded-md hover:bg-accent transition-colors"
>
Reject All
</button>
<button
onClick={saveCustom}
className="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
>
Save Preferences
</button>
</div>
</div>
)}
</div>
</div>
);
}
// Hook to check consent status from other components
export function useGDPRConsent() {
const [consent, setConsent] = useState<ConsentState | null>(null);
useEffect(() => {
const savedConsent = localStorage.getItem(CONSENT_COOKIE_NAME);
if (savedConsent) {
try {
setConsent(JSON.parse(savedConsent));
} catch {
setConsent(null);
}
}
}, []);
return {
hasConsented: consent !== null,
analyticsEnabled: consent?.analytics ?? false,
preferencesEnabled: consent?.preferences ?? false,
resetConsent: () => {
localStorage.removeItem(CONSENT_COOKIE_NAME);
window.location.reload();
},
};
}
// Component to conditionally render based on consent
export function ConsentGate({
children,
type,
fallback,
}: {
children: React.ReactNode;
type: "analytics" | "preferences";
fallback?: React.ReactNode;
}) {
const { analyticsEnabled, preferencesEnabled } = useGDPRConsent();
const hasConsent = type === "analytics" ? analyticsEnabled : preferencesEnabled;
if (!hasConsent) {
return fallback || null;
}
return <>{children}</>;
}