wwwfiberdirekt/src/components/CookieConsent.tsx
2026-02-18 14:13:25 +01:00

183 lines
6.3 KiB
TypeScript

'use client'
import React, { useState, useEffect, useCallback } from 'react'
const COOKIE_NAME = 'fd-cookie-consent'
const COOKIE_TIMESTAMP_NAME = 'fd-cookie-consent-ts'
// Default expiry durations (in days)
// 0 = session only (sessionStorage, not persisted across browser close)
const DEFAULT_ACCEPTED_DAYS = 365
const DEFAULT_DECLINED_DAYS = 30
type ConsentStatus = 'accepted' | 'declined' | null
function getCookie(name: string): string | null {
if (typeof document === 'undefined') return null
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`))
return match ? decodeURIComponent(match[1]) : null
}
function setCookie(name: string, value: string, days: number) {
if (days === 0) {
// Session cookie — no expires attribute
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; SameSite=Lax`
} else {
const expires = new Date(Date.now() + days * 864e5).toUTCString()
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax`
}
}
function deleteCookie(name: string) {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`
}
/**
* Check whether the stored consent is still valid for the given number of days.
* If days = 0, we rely purely on the session cookie existing (no timestamp check).
*/
function isConsentExpired(days: number): boolean {
if (days === 0) return false // Session cookies handle their own expiry
const ts = getCookie(COOKIE_TIMESTAMP_NAME)
if (!ts) return true
const storedAt = parseInt(ts, 10)
if (isNaN(storedAt)) return true
return Date.now() - storedAt > days * 864e5
}
interface CookieConsentProps {
privacyPolicyUrl?: string
/** Days to remember "Acceptera" — 0 = browser session only */
acceptedDays?: number
/** Days to remember "Avvisa" before asking again — 0 = browser session only */
declinedDays?: number
onAccept?: () => void
onDecline?: () => void
}
export const CookieConsent: React.FC<CookieConsentProps> = ({
privacyPolicyUrl = '/integritetspolicy',
acceptedDays = DEFAULT_ACCEPTED_DAYS,
declinedDays = DEFAULT_DECLINED_DAYS,
onAccept,
onDecline,
}) => {
const [visible, setVisible] = useState(false)
useEffect(() => {
const consent = getCookie(COOKIE_NAME)
if (!consent) {
// No consent stored at all — show the banner
const timer = setTimeout(() => setVisible(true), 800)
return () => clearTimeout(timer)
}
// Consent exists — check if it has expired for its type
const expiryDays = consent === 'accepted' ? acceptedDays : declinedDays
if (isConsentExpired(expiryDays)) {
// Consent period has lapsed — clear old cookies and ask again
deleteCookie(COOKIE_NAME)
deleteCookie(COOKIE_TIMESTAMP_NAME)
const timer = setTimeout(() => setVisible(true), 800)
return () => clearTimeout(timer)
}
}, [acceptedDays, declinedDays])
const handleAccept = useCallback(() => {
setCookie(COOKIE_NAME, 'accepted', acceptedDays)
setCookie(COOKIE_TIMESTAMP_NAME, String(Date.now()), acceptedDays)
setVisible(false)
if (typeof window !== 'undefined' && window._paq) {
window._paq.push(['setConsentGiven'])
}
onAccept?.()
}, [acceptedDays, onAccept])
const handleDecline = useCallback(() => {
setCookie(COOKIE_NAME, 'declined', declinedDays)
setCookie(COOKIE_TIMESTAMP_NAME, String(Date.now()), declinedDays)
setVisible(false)
if (typeof window !== 'undefined' && window._paq) {
window._paq.push(['forgetConsentGiven'])
}
onDecline?.()
}, [declinedDays, onDecline])
if (!visible) return null
const policyLink = (
<a
href={privacyPolicyUrl}
className="text-fd-yellow underline hover:text-fd-yellow/80 transition-colors"
>
Läs vår integritetspolicy
</a>
)
return (
<div
className="fixed bottom-0 left-0 right-0 z-[9999] w-full bg-fd-navy shadow-[0_-4px_32px_rgba(0,0,0,0.35)]"
role="dialog"
aria-label="Cookiesamtycke"
>
{/* Inner layout — full-width, no rounded corners */}
<div className="w-full px-6 py-6 md:px-12 md:py-8 lg:px-20">
<div className="max-w-[1400px] mx-auto flex flex-col md:flex-row md:items-center gap-5 md:gap-8">
{/* Text block */}
<div className="flex-1 min-w-0">
<h3 className="text-fd-yellow font-joey-bold text-lg md:text-xl mb-1.5">
Vi använder cookies
</h3>
<p className="text-white/85 font-joey text-sm md:text-base leading-relaxed">
Vi använder cookies för att förbättra din upplevelse vår webbplats och analysera
trafik. Genom att klicka &quot;Acceptera&quot; godkänner du vår användning av
cookies.{' '}
{policyLink}
</p>
</div>
{/* Action buttons */}
<div className="flex flex-row gap-3 flex-shrink-0">
<button
onClick={handleDecline}
className="inline-flex items-center justify-center px-6 py-2.5 font-joey-bold text-white border-2 border-white/60 hover:border-white hover:bg-white/10 transition-all text-sm md:text-base rounded-full"
>
Avvisa
</button>
<button
onClick={handleAccept}
className="inline-flex items-center justify-center px-6 py-2.5 font-joey-bold text-fd-navy bg-fd-yellow hover:bg-fd-yellow/90 transition-colors text-sm md:text-base rounded-full"
>
Acceptera
</button>
</div>
</div>
</div>
</div>
)
}
// ─── Utility exports (unchanged) ─────────────────────────────────────────────
export function getConsentStatus(): ConsentStatus {
const value = getCookie(COOKIE_NAME)
if (value === 'accepted' || value === 'declined') return value
return null
}
export function loadScriptIfConsented(src: string, attributes?: Record<string, string>): boolean {
if (getConsentStatus() !== 'accepted') return false
if (document.querySelector(`script[src="${src}"]`)) return true
const script = document.createElement('script')
script.src = src
script.async = true
if (attributes) {
Object.entries(attributes).forEach(([key, val]) => script.setAttribute(key, val))
}
document.head.appendChild(script)
return true
}