276 lines
9.2 KiB
TypeScript
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}</>;
|
|
}
|