183 lines
6.3 KiB
TypeScript
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 på vår webbplats och analysera
|
|
trafik. Genom att klicka "Acceptera" 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
|
|
}
|