503 lines
24 KiB
PHP
503 lines
24 KiB
PHP
@props([
|
|
'analyticsId' => config('cookie-consent.analytics_id'),
|
|
'gtmId' => config('cookie-consent.gtm_id'),
|
|
'enabled' => config('cookie-consent.enabled', true),
|
|
'cookieLifetime' => config('cookie-consent.cookie_lifetime', 365),
|
|
'cookieName' => config('cookie-consent.cookie_name', 'cookie_consent'),
|
|
'links' => config('cookie-consent.links', []),
|
|
'buttonPosition' => config('cookie-consent.button_position', 'bottom-left'),
|
|
'anonymizeIp' => config('cookie-consent.anonymize_ip', true),
|
|
'colors' => config('cookie-consent.colors', []),
|
|
])
|
|
|
|
@php
|
|
$links = array_merge(
|
|
[
|
|
'privacy' => '/datenschutz',
|
|
'imprint' => '/impressum',
|
|
],
|
|
$links ?? [],
|
|
);
|
|
|
|
$colors = array_merge(
|
|
[
|
|
'primary' => '#009bdd',
|
|
'primary_hover' => '#0071a8',
|
|
'accept' => '#16a34a',
|
|
'accept_hover' => '#15803d',
|
|
'save' => '#1f2937',
|
|
'save_hover' => '#111827',
|
|
'button_bg' => '#1f2937',
|
|
'button_hover' => '#374151',
|
|
],
|
|
$colors ?? [],
|
|
);
|
|
|
|
$positionClasses = $buttonPosition === 'bottom-right' ? 'right-4' : 'left-4';
|
|
|
|
// Bestimme ob GTM oder Analytics verwendet wird (GTM hat Priorität)
|
|
$useGtm = !empty($gtmId);
|
|
$useAnalytics = !empty($analyticsId) && !$useGtm;
|
|
$hasTracking = $useGtm || $useAnalytics;
|
|
@endphp
|
|
|
|
@if ($enabled)
|
|
{{-- CSS-Variablen für Farben --}}
|
|
<style>
|
|
.cookie-consent-wrapper {
|
|
--cc-primary: {{ $colors['primary'] }};
|
|
--cc-primary-hover: {{ $colors['primary_hover'] }};
|
|
--cc-accept: {{ $colors['accept'] }};
|
|
--cc-accept-hover: {{ $colors['accept_hover'] }};
|
|
--cc-save: {{ $colors['save'] }};
|
|
--cc-save-hover: {{ $colors['save_hover'] }};
|
|
--cc-button-bg: {{ $colors['button_bg'] }};
|
|
--cc-button-hover: {{ $colors['button_hover'] }};
|
|
}
|
|
|
|
.cookie-consent-wrapper .cc-btn-accept {
|
|
background-color: var(--cc-accept);
|
|
}
|
|
|
|
.cookie-consent-wrapper .cc-btn-accept:hover {
|
|
background-color: var(--cc-accept-hover);
|
|
}
|
|
|
|
.cookie-consent-wrapper .cc-btn-save {
|
|
background-color: var(--cc-save);
|
|
}
|
|
|
|
.cookie-consent-wrapper .cc-btn-save:hover {
|
|
background-color: var(--cc-save-hover);
|
|
}
|
|
|
|
.cookie-consent-wrapper .cc-floating-btn {
|
|
background-color: var(--cc-button-bg);
|
|
}
|
|
|
|
.cookie-consent-wrapper .cc-floating-btn:hover {
|
|
background-color: var(--cc-button-hover);
|
|
}
|
|
|
|
.cookie-consent-wrapper .cc-icon-bg {
|
|
background-color: color-mix(in srgb, var(--cc-primary) 15%, transparent);
|
|
}
|
|
|
|
.cookie-consent-wrapper .cc-icon-color {
|
|
color: var(--cc-primary);
|
|
}
|
|
|
|
.cookie-consent-wrapper .cc-toggle-active {
|
|
background-color: var(--cc-primary);
|
|
}
|
|
|
|
.cookie-consent-wrapper .cc-toggle-inactive {
|
|
background-color: #d1d5db;
|
|
}
|
|
|
|
.cookie-consent-wrapper .cc-link:hover {
|
|
color: var(--cc-primary);
|
|
}
|
|
</style>
|
|
|
|
{{-- Google Consent Mode v2: Default-Einstellungen VOR dem Laden von GA --}}
|
|
<script>
|
|
window.dataLayer = window.dataLayer || [];
|
|
|
|
function gtag() {
|
|
dataLayer.push(arguments);
|
|
}
|
|
|
|
// Consent Mode v2 Defaults - alles verweigert bis Nutzer entscheidet
|
|
gtag('consent', 'default', {
|
|
'analytics_storage': 'denied',
|
|
'ad_storage': 'denied',
|
|
'ad_user_data': 'denied',
|
|
'ad_personalization': 'denied',
|
|
'functionality_storage': 'granted',
|
|
'personalization_storage': 'denied',
|
|
'security_storage': 'granted',
|
|
'wait_for_update': 500
|
|
});
|
|
</script>
|
|
|
|
<div x-data="cookieConsentManager({
|
|
analyticsId: '{{ $analyticsId }}',
|
|
gtmId: '{{ $gtmId }}',
|
|
useGtm: {{ $useGtm ? 'true' : 'false' }},
|
|
cookieLifetime: {{ $cookieLifetime }},
|
|
cookieName: '{{ $cookieName }}',
|
|
anonymizeIp: {{ $anonymizeIp ? 'true' : 'false' }}
|
|
})" x-init="init()" @open-cookie-settings.window="openModal()"
|
|
class="cookie-consent-wrapper">
|
|
{{-- Floating Button --}}
|
|
<button x-show="!isOpen && hasConsented" x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 scale-75" x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-200" x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-75" @click="openModal()"
|
|
class="cc-floating-btn fixed bottom-2 {{ $positionClasses }} z-40 text-white p-3 rounded-full shadow-lg hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-300 cursor-pointer"
|
|
style="--tw-ring-color: {{ $colors['primary'] }}"
|
|
aria-label="{{ __('cookie-consent::cookie-consent.aria.open_settings') }}"
|
|
title="{{ __('cookie-consent::cookie-consent.modal.title') }}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor" aria-hidden="true">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
|
</svg>
|
|
</button>
|
|
|
|
{{-- Modal Backdrop + Dialog --}}
|
|
<div x-show="isOpen" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
|
class="fixed inset-0 z-50 overflow-y-auto" style="display: none;" role="dialog" aria-modal="true"
|
|
aria-labelledby="cookie-consent-title">
|
|
{{-- Backdrop --}}
|
|
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm" @click="closeIfAllowed()"></div>
|
|
|
|
{{-- Dialog Container --}}
|
|
<div class="flex min-h-full items-center justify-center p-4">
|
|
<div x-show="isOpen" x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
|
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
class="relative bg-white rounded-xl shadow-2xl max-w-lg w-full overflow-hidden"
|
|
@click.away="closeIfAllowed()" x-trap.noscroll="isOpen">
|
|
{{-- Header --}}
|
|
<div class="px-6 pt-6 pb-4">
|
|
<div class="flex items-start gap-4">
|
|
<div
|
|
class="cc-icon-bg flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="cc-icon-color h-5 w-5" fill="none"
|
|
viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<h2 id="cookie-consent-title" class="text-xl font-bold text-gray-900">
|
|
{{ __('cookie-consent::cookie-consent.modal.title') }}
|
|
</h2>
|
|
<p class="mt-1 text-sm text-gray-600">
|
|
{{ __('cookie-consent::cookie-consent.modal.description') }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Cookie Options --}}
|
|
<div class="px-6 py-4 bg-gray-50 border-y border-gray-200 space-y-4">
|
|
{{-- Essenzielle Cookies (immer aktiv) --}}
|
|
<div class="flex items-center justify-between gap-4">
|
|
<div class="flex-1 min-w-0">
|
|
<label class="font-medium text-gray-900">
|
|
{{ __('cookie-consent::cookie-consent.categories.essential.title') }}
|
|
</label>
|
|
<p class="text-xs text-gray-500 mt-0.5">
|
|
{{ __('cookie-consent::cookie-consent.categories.essential.description') }}
|
|
</p>
|
|
</div>
|
|
<div class="flex-shrink-0">
|
|
<button type="button" disabled
|
|
class="cc-toggle-active relative inline-flex h-6 w-11 flex-shrink-0 cursor-not-allowed rounded-full opacity-60 cursor-pointer"
|
|
role="switch" aria-checked="true"
|
|
aria-label="{{ __('cookie-consent::cookie-consent.aria.essential_always_active') }}">
|
|
<span
|
|
class="translate-x-5 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out mt-0.5"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Analytics Cookies (optional, nur wenn Analytics ID oder GTM ID vorhanden) --}}
|
|
@if ($hasTracking)
|
|
<div class="flex items-center justify-between gap-4">
|
|
<div class="flex-1 min-w-0">
|
|
<label for="analytics-toggle" class="font-medium text-gray-900 cursor-pointer">
|
|
{{ __('cookie-consent::cookie-consent.categories.analytics.title') }}
|
|
</label>
|
|
<p class="text-xs text-gray-500 mt-0.5">
|
|
{{ __('cookie-consent::cookie-consent.categories.analytics.description') }}
|
|
</p>
|
|
</div>
|
|
<div class="flex-shrink-0">
|
|
<button type="button" id="analytics-toggle"
|
|
@click="tempSettings.analytics = !tempSettings.analytics"
|
|
:class="tempSettings.analytics ? 'cc-toggle-active' : 'cc-toggle-inactive'"
|
|
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 cursor-pointer"
|
|
:style="tempSettings.analytics ? '--tw-ring-color: {{ $colors['primary'] }}' : ''"
|
|
role="switch" :aria-checked="tempSettings.analytics.toString()"
|
|
aria-label="{{ __('cookie-consent::cookie-consent.aria.toggle_analytics') }}">
|
|
<span :class="tempSettings.analytics ? 'translate-x-5' : 'translate-x-0.5'"
|
|
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out mt-0.5"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- Footer Links --}}
|
|
<div class="px-6 py-3 bg-gray-50 border-b border-gray-200">
|
|
<div class="flex items-center justify-center gap-4 text-xs text-gray-500">
|
|
<a href="{{ $links['privacy'] }}" class="cc-link hover:underline transition-colors">
|
|
{{ __('cookie-consent::cookie-consent.links.privacy') }}
|
|
</a>
|
|
<span class="text-gray-300">|</span>
|
|
<a href="{{ $links['imprint'] }}" class="cc-link hover:underline transition-colors">
|
|
{{ __('cookie-consent::cookie-consent.links.imprint') }}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Action Buttons --}}
|
|
<div class="px-6 py-4 bg-white">
|
|
<div class="flex flex-col sm:flex-row gap-3">
|
|
<button @click="acceptAll()"
|
|
class="cc-btn-accept flex-1 inline-flex justify-center items-center px-4 py-2.5 border border-transparent text-sm font-semibold rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors duration-200 cursor-pointer"
|
|
style="--tw-ring-color: {{ $colors['accept'] }}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none"
|
|
viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
{{ __('cookie-consent::cookie-consent.buttons.accept_all') }}
|
|
</button>
|
|
<button @click="saveSettings()"
|
|
class="cc-btn-save flex-1 inline-flex justify-center items-center px-4 py-2.5 border border-transparent text-sm font-semibold rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors duration-200 cursor-pointer"
|
|
style="--tw-ring-color: {{ $colors['save'] }}">
|
|
{{ __('cookie-consent::cookie-consent.buttons.save_selection') }}
|
|
</button>
|
|
<button @click="rejectAll()"
|
|
class="flex-1 inline-flex justify-center items-center px-4 py-2.5 border border-gray-300 text-sm font-semibold rounded-lg text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-colors duration-200 cursor-pointer">
|
|
{{ __('cookie-consent::cookie-consent.buttons.reject_all') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Cookie Consent Manager Script --}}
|
|
<script>
|
|
function cookieConsentManager(config) {
|
|
return {
|
|
isOpen: false,
|
|
hasConsented: false,
|
|
analyticsLoaded: false,
|
|
gtmLoaded: false,
|
|
config: config,
|
|
|
|
settings: {
|
|
analytics: false,
|
|
},
|
|
tempSettings: {
|
|
analytics: false,
|
|
},
|
|
|
|
init() {
|
|
const saved = this.getCookie(this.config.cookieName);
|
|
|
|
if (saved) {
|
|
try {
|
|
this.settings = JSON.parse(saved);
|
|
this.tempSettings = {
|
|
...this.settings
|
|
};
|
|
this.hasConsented = true;
|
|
this.isOpen = false;
|
|
|
|
// Consent Mode aktualisieren und ggf. Analytics laden
|
|
this.applyConsent();
|
|
} catch (e) {
|
|
console.error('Cookie Consent: Error parsing settings', e);
|
|
this.isOpen = true;
|
|
}
|
|
} else {
|
|
// Erstbesuch -> Modal öffnen
|
|
this.isOpen = true;
|
|
}
|
|
|
|
// Prüfen ob ein Cookie-Blocker-Plugin das Modal versteckt hat
|
|
this.$nextTick(() => {
|
|
this.checkVisibility();
|
|
});
|
|
},
|
|
|
|
checkVisibility() {
|
|
if (!this.isOpen) return;
|
|
|
|
const wrapper = this.$root;
|
|
if (!wrapper) return;
|
|
|
|
// Prüfen ob das Element oder ein Eltern-Element versteckt wurde
|
|
const computedStyle = window.getComputedStyle(wrapper);
|
|
const isHidden = computedStyle.display === 'none' ||
|
|
computedStyle.visibility === 'hidden' ||
|
|
wrapper.offsetParent === null;
|
|
|
|
if (isHidden) {
|
|
console.log('Cookie Consent: Banner wurde durch externes Plugin versteckt, Scroll freigeben');
|
|
this.isOpen = false;
|
|
this.hasConsented = true; // Verhindert erneutes Öffnen
|
|
}
|
|
},
|
|
|
|
openModal() {
|
|
this.tempSettings = {
|
|
...this.settings
|
|
};
|
|
this.isOpen = true;
|
|
},
|
|
|
|
closeIfAllowed() {
|
|
// Modal nur schließen wenn bereits eine Entscheidung getroffen wurde
|
|
if (this.hasConsented) {
|
|
this.isOpen = false;
|
|
}
|
|
},
|
|
|
|
acceptAll() {
|
|
this.settings.analytics = true;
|
|
this.save();
|
|
},
|
|
|
|
rejectAll() {
|
|
this.settings.analytics = false;
|
|
this.save();
|
|
},
|
|
|
|
saveSettings() {
|
|
this.settings = {
|
|
...this.tempSettings
|
|
};
|
|
this.save();
|
|
},
|
|
|
|
save() {
|
|
this.setCookie(
|
|
this.config.cookieName,
|
|
JSON.stringify(this.settings),
|
|
this.config.cookieLifetime
|
|
);
|
|
this.hasConsented = true;
|
|
this.isOpen = false;
|
|
this.applyConsent();
|
|
|
|
// Event für andere Komponenten (z.B. Privacy-Info) dispatchen
|
|
window.dispatchEvent(new CustomEvent('cookie-consent-updated', {
|
|
detail: this.settings
|
|
}));
|
|
},
|
|
|
|
applyConsent() {
|
|
const analyticsStatus = this.settings.analytics ? 'granted' : 'denied';
|
|
|
|
// Google Consent Mode v2 Update
|
|
if (typeof gtag === 'function') {
|
|
gtag('consent', 'update', {
|
|
'analytics_storage': analyticsStatus,
|
|
'ad_storage': analyticsStatus,
|
|
'ad_user_data': analyticsStatus,
|
|
'ad_personalization': analyticsStatus
|
|
});
|
|
}
|
|
|
|
// Tracking laden wenn zugestimmt
|
|
if (this.settings.analytics) {
|
|
// GTM hat Priorität über direktes Analytics
|
|
if (this.config.useGtm && this.config.gtmId && !this.gtmLoaded) {
|
|
this.loadGoogleTagManager();
|
|
} else if (!this.config.useGtm && this.config.analyticsId && !this.analyticsLoaded) {
|
|
this.loadGoogleAnalytics();
|
|
}
|
|
}
|
|
},
|
|
|
|
loadGoogleAnalytics() {
|
|
if (this.analyticsLoaded || !this.config.analyticsId) return;
|
|
|
|
// gtag.js Script laden
|
|
const script = document.createElement('script');
|
|
script.async = true;
|
|
script.src = `https://www.googletagmanager.com/gtag/js?id=${this.config.analyticsId}`;
|
|
script.onload = () => {
|
|
gtag('js', new Date());
|
|
|
|
const configOptions = {
|
|
'send_page_view': true
|
|
};
|
|
|
|
if (this.config.anonymizeIp) {
|
|
configOptions['anonymize_ip'] = true;
|
|
}
|
|
|
|
gtag('config', this.config.analyticsId, configOptions);
|
|
this.analyticsLoaded = true;
|
|
|
|
console.log('Cookie Consent: Google Analytics loaded');
|
|
};
|
|
|
|
document.head.appendChild(script);
|
|
},
|
|
|
|
loadGoogleTagManager() {
|
|
if (this.gtmLoaded || !this.config.gtmId) return;
|
|
|
|
// Google Tag Manager Script laden
|
|
(function(w, d, s, l, i) {
|
|
w[l] = w[l] || [];
|
|
w[l].push({
|
|
'gtm.start': new Date().getTime(),
|
|
event: 'gtm.js'
|
|
});
|
|
var f = d.getElementsByTagName(s)[0],
|
|
j = d.createElement(s),
|
|
dl = l != 'dataLayer' ? '&l=' + l : '';
|
|
j.async = true;
|
|
j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl;
|
|
f.parentNode.insertBefore(j, f);
|
|
})(window, document, 'script', 'dataLayer', this.config.gtmId);
|
|
|
|
this.gtmLoaded = true;
|
|
|
|
// Event für GTM noscript-Komponente dispatchen
|
|
window.dispatchEvent(new CustomEvent('gtm-consent-granted', {
|
|
detail: {
|
|
gtmId: this.config.gtmId
|
|
}
|
|
}));
|
|
|
|
console.log('Cookie Consent: Google Tag Manager loaded');
|
|
},
|
|
|
|
// Cookie Helper Functions
|
|
setCookie(name, value, days) {
|
|
const date = new Date();
|
|
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
const expires = "expires=" + date.toUTCString();
|
|
const sameSite = "SameSite=Lax";
|
|
const secure = window.location.protocol === 'https:' ? '; Secure' : '';
|
|
document.cookie = `${name}=${encodeURIComponent(value)}; ${expires}; path=/; ${sameSite}${secure}`;
|
|
},
|
|
|
|
getCookie(name) {
|
|
const nameEQ = name + "=";
|
|
const ca = document.cookie.split(';');
|
|
for (let i = 0; i < ca.length; i++) {
|
|
let c = ca[i].trim();
|
|
if (c.indexOf(nameEQ) === 0) {
|
|
return decodeURIComponent(c.substring(nameEQ.length));
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
deleteCookie(name) {
|
|
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
@endif
|