From 3b1c9821a3204e2da1429004d95bc847cd4cc231 Mon Sep 17 00:00:00 2001 From: Remi <73385395+itsrubberduck@users.noreply.github.com> Date: Wed, 17 Sep 2025 17:45:00 +0200 Subject: [PATCH] Add GDPR-compliant Hotjar tracking with consent banner --- app/app.vue | 4 + app/components/CookieConsentBanner.vue | 401 +++++++++++++++++++++++++ app/composables/useCookieConsent.ts | 79 +++++ 3 files changed, 484 insertions(+) create mode 100644 app/app.vue create mode 100644 app/components/CookieConsentBanner.vue create mode 100644 app/composables/useCookieConsent.ts diff --git a/app/app.vue b/app/app.vue new file mode 100644 index 0000000..b825d35 --- /dev/null +++ b/app/app.vue @@ -0,0 +1,4 @@ + diff --git a/app/components/CookieConsentBanner.vue b/app/components/CookieConsentBanner.vue new file mode 100644 index 0000000..c53670f --- /dev/null +++ b/app/components/CookieConsentBanner.vue @@ -0,0 +1,401 @@ + + + + + diff --git a/app/composables/useCookieConsent.ts b/app/composables/useCookieConsent.ts new file mode 100644 index 0000000..726652a --- /dev/null +++ b/app/composables/useCookieConsent.ts @@ -0,0 +1,79 @@ +import { computed, watch } from 'vue'; + +type CookieConsentPreferences = { + necessary: true; + analytics: boolean; +}; + +type CookieConsentValue = { + version: number; + updatedAt: string; + preferences: CookieConsentPreferences; +}; + +const CONSENT_COOKIE_NAME = 'osq-cookie-consent'; +const CONSENT_VERSION = 1; +const SIX_MONTHS_IN_SECONDS = 60 * 60 * 24 * 180; + +export const useCookieConsent = () => { + const consentCookie = useCookie(CONSENT_COOKIE_NAME, { + sameSite: 'lax', + path: '/', + maxAge: SIX_MONTHS_IN_SECONDS, + secure: !process.dev, + }); + + if (consentCookie.value && consentCookie.value.version !== CONSENT_VERSION) { + consentCookie.value = null; + } + + const consentState = useState('cookie-consent', () => consentCookie.value ?? null); + + watch( + consentState, + (value) => { + consentCookie.value = value; + }, + { deep: true } + ); + + const hasConsent = computed(() => consentState.value !== null); + + const analyticsEnabled = computed(() => { + if (!consentState.value) { + return false; + } + + return consentState.value.preferences.analytics === true; + }); + + const savePreferences = (preferences: { analytics: boolean }) => { + consentState.value = { + version: CONSENT_VERSION, + updatedAt: new Date().toISOString(), + preferences: { + necessary: true, + analytics: preferences.analytics, + }, + }; + }; + + const acceptAll = () => savePreferences({ analytics: true }); + + const rejectAll = () => savePreferences({ analytics: false }); + + const resetConsent = () => { + consentState.value = null; + consentCookie.value = null; + }; + + return { + consent: consentState, + hasConsent, + analyticsEnabled, + acceptAll, + rejectAll, + savePreferences, + resetConsent, + }; +};