feat: complete cookie consent with categories & technical processing

- Full 4-category consent modal: Necesare (locked) / Funcționale /
  Analitice / Marketing with toggle switches per category
- Banner shows simple view first; "Personalizează" opens detailed panel
- Consent stored as JSON in localStorage (cookie_consent_v2)
- window.cookieConsent object set on load; dispatches
  cookie-consent-updated / cookie-analytics-granted / cookie-marketing-granted
  CustomEvents for deferred script loading
- Floating 🍪 FAB button lets users re-open settings at any time
- Policy page: "Personalizează" button resets consent, reopens modal
- Full RO/RU translation for all categories and UI strings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 20:39:13 +00:00
parent 68f58cbcdc
commit c3e4b99d4b
5 changed files with 284 additions and 100 deletions
+22 -2
View File
@@ -157,10 +157,30 @@ return [
'cookie' => [
'policy_title' => 'Politica de utilizare a fișierelor cookie',
'back' => 'Înapoi',
'manage_title' => 'Gestionează setările cookie',
// Banner (simple view)
'banner_title' => 'Utilizăm fișiere cookie',
'banner_body' => 'Folosim module cookie pentru funcționarea site-ului și îmbunătățirea experienței. Cookies strict necesare sunt utilizate fără consimțământ (Legea nr. 195/2024).',
'banner_body' => 'Utilizăm cookie-uri pentru funcționarea site-ului și îmbunătățirea experienței dvs. Cookie-urile strict necesare nu necesită consimțământ (Legea nr. 195/2024).',
'read_policy' => 'Politica cookie',
'accept' => 'Acceptă toate',
'reject' => 'Doar esențiale',
'reject' => 'Respinge opționale',
'customize' => 'Personalizează',
// Settings modal
'settings_title' => 'Setări cookie',
'settings_body' => 'Alegeți ce categorii de cookie-uri acceptați. Puteți modifica preferințele oricând.',
'save_settings' => 'Salvează preferințele',
'always_on' => 'Întotdeauna activ',
// Categories
'cat_necessary' => 'Cookie-uri strict necesare',
'cat_necessary_desc' => 'Indispensabile pentru funcționarea corectă a site-ului: autentificare, sesiune, securitate, preferințe limbă. Nu pot fi dezactivate.',
'cat_functional' => 'Cookie-uri funcționale',
'cat_functional_desc'=> 'Permit site-ului să rețină alegerile dvs. (limbă, preferințe de afișare) pentru o experiență mai bună.',
'cat_analytics' => 'Cookie-uri analitice',
'cat_analytics_desc' => 'Ne ajută să înțelegem cum utilizați site-ul (pagini vizitate, surse de trafic) pentru a îmbunătăți serviciile. Date anonimizate.',
'cat_marketing' => 'Cookie-uri de marketing',
'cat_marketing_desc' => 'Utilizate pentru publicitate relevantă și evaluarea eficienței campaniilor. Pot fi setate de terți (Google Ads etc.).',
],
];
+22 -2
View File
@@ -157,10 +157,30 @@ return [
'cookie' => [
'policy_title' => 'Политика использования cookie-файлов',
'back' => 'Назад',
'manage_title' => 'Управление настройками cookie',
// Баннер (простой вид)
'banner_title' => 'Мы используем cookie',
'banner_body' => 'Мы используем cookie-файлы для работы сайта и улучшения вашего опыта. Строго необходимые cookie используются без согласия (Закон № 195/2024).',
'banner_body' => 'Мы используем cookie-файлы для работы сайта и улучшения вашего опыта. Строго необходимые cookie не требуют согласия (Закон № 195/2024).',
'read_policy' => 'Политика cookie',
'accept' => 'Принять все',
'reject' => 'Только необходимые',
'reject' => 'Отклонить необязательные',
'customize' => 'Настроить',
// Модальное окно настроек
'settings_title' => 'Настройки cookie',
'settings_body' => 'Выберите, какие категории cookie-файлов вы принимаете. Вы можете изменить настройки в любое время.',
'save_settings' => 'Сохранить настройки',
'always_on' => 'Всегда активно',
// Категории
'cat_necessary' => 'Строго необходимые cookie',
'cat_necessary_desc' => 'Необходимы для корректной работы сайта: аутентификация, сессия, безопасность, языковые настройки. Не могут быть отключены.',
'cat_functional' => 'Функциональные cookie',
'cat_functional_desc'=> 'Позволяют сайту запоминать ваши предпочтения (язык, настройки отображения) для более удобного использования.',
'cat_analytics' => 'Аналитические cookie',
'cat_analytics_desc' => 'Помогают нам понять, как вы используете сайт (посещённые страницы, источники трафика), для улучшения сервиса. Данные анонимизированы.',
'cat_marketing' => 'Маркетинговые cookie',
'cat_marketing_desc' => 'Используются для релевантной рекламы и оценки эффективности кампаний. Могут устанавливаться третьими лицами (Google Ads и др.).',
],
];
+199 -79
View File
@@ -1,90 +1,210 @@
{{-- Cookie consent banner — localStorage-based, shown once per browser --}}
{{-- Full cookie consent — GDPR/Legea 195/2024 compliant --}}
@php
$bannerEnabled = data_get($tenant->settings ?? [], 'cookie.banner_enabled', true);
$tc = $themeColor ?? '#3B82F6';
@endphp
@if ($bannerEnabled)
<style>
[x-cloak]{display:none!important}
.ck-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:10000;display:flex;align-items:flex-end;justify-content:center;padding:0}
@media(min-width:600px){.ck-overlay{align-items:center;padding:20px}}
.ck-modal{background:#fff;width:100%;max-width:560px;border-radius:16px 16px 0 0;padding:24px 20px 20px;max-height:90vh;overflow-y:auto;box-shadow:0 -8px 40px rgba(0,0,0,.18);font-family:system-ui,-apple-system,'Segoe UI',sans-serif}
@media(min-width:600px){.ck-modal{border-radius:16px;max-height:80vh}}
.ck-modal h2{font-size:17px;font-weight:700;color:#111827;margin-bottom:6px}
.ck-modal .ck-intro{font-size:13px;color:#6b7280;margin-bottom:16px;line-height:1.5}
.ck-cat{border:1px solid #e5e7eb;border-radius:10px;padding:14px 14px 12px;margin-bottom:8px}
.ck-cat-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
.ck-cat-head label{font-size:14px;font-weight:600;color:#111827;cursor:pointer;flex:1}
.ck-cat p{font-size:12px;color:#6b7280;margin-top:8px;line-height:1.5}
.ck-toggle{position:relative;width:40px;height:22px;flex-shrink:0}
.ck-toggle input{opacity:0;width:0;height:0;position:absolute}
.ck-slider{position:absolute;inset:0;border-radius:22px;background:#d1d5db;transition:background .2s;cursor:pointer}
.ck-slider:before{content:'';position:absolute;width:16px;height:16px;left:3px;top:3px;border-radius:50%;background:#fff;transition:transform .2s;box-shadow:0 1px 3px rgba(0,0,0,.2)}
input:checked+.ck-slider{background:{{ $tc }}}
input:checked+.ck-slider:before{transform:translateX(18px)}
input:disabled+.ck-slider{opacity:.6;cursor:default}
.ck-badge{font-size:11px;background:#f3f4f6;color:#6b7280;border-radius:4px;padding:1px 7px;font-weight:500}
.ck-badge.always{background:#dcfce7;color:#166534}
.ck-btns{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap}
.ck-btn{flex:1;min-width:120px;padding:10px 12px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;border:none;white-space:nowrap;text-align:center}
.ck-btn-primary{background:{{ $tc }};color:#fff}
.ck-btn-secondary{background:#f3f4f6;color:#374151;border:1px solid #e5e7eb}
.ck-btn-ghost{background:transparent;color:#6b7280;border:1px solid #d1d5db}
.ck-link{color:{{ $tc }};text-decoration:none;font-size:12px}
.ck-link:hover{opacity:.8}
.ck-footer{display:flex;justify-content:space-between;align-items:center;margin-top:14px;padding-top:12px;border-top:1px solid #f3f4f6;flex-wrap:wrap;gap:8px}
/* Floating re-open button */
.ck-fab{position:fixed;bottom:16px;left:16px;z-index:9000;background:#fff;border:1px solid #e5e7eb;border-radius:50%;width:44px;height:44px;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,.12);font-size:20px;transition:box-shadow .15s}
.ck-fab:hover{box-shadow:0 4px 16px rgba(0,0,0,.18)}
@media(prefers-color-scheme:dark){
.ck-modal{background:#1e293b;color:#f1f5f9}
.ck-modal h2{color:#f1f5f9}
.ck-modal .ck-intro{color:#94a3b8}
.ck-cat{border-color:#334155}
.ck-cat-head label{color:#f1f5f9}
.ck-cat p{color:#94a3b8}
.ck-slider{background:#475569}
.ck-badge{background:#334155;color:#94a3b8}
.ck-badge.always{background:#14532d;color:#86efac}
.ck-btn-secondary{background:#334155;color:#f1f5f9;border-color:#475569}
.ck-btn-ghost{color:#94a3b8;border-color:#475569}
.ck-footer{border-color:#334155}
.ck-fab{background:#1e293b;border-color:#334155;color:#f1f5f9}
}
</style>
<div
x-data="{
show: false,
init() {
if (!localStorage.getItem('cookie_consent')) {
this.show = true;
}
},
accept() {
localStorage.setItem('cookie_consent', 'accepted');
this.show = false;
},
reject() {
localStorage.setItem('cookie_consent', 'essential_only');
this.show = false;
}
}"
x-show="show"
x-cloak
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-4"
style="
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
background: #1f2937;
color: #f9fafb;
padding: 16px 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
box-shadow: 0 -4px 20px rgba(0,0,0,.25);
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
line-height: 1.5;
"
x-data="cookieConsent()"
x-init="init()"
>
<div style="flex:1;min-width:200px;">
<span style="font-weight:600;margin-right:6px;">🍪 {{ __('portal.cookie.banner_title') }}</span>
{{-- Backdrop + Modal --}}
<div class="ck-overlay" x-show="open" x-cloak @click.self="open=false">
<div class="ck-modal" @click.stop>
<div x-show="!showDetails">
{{-- Simple banner view --}}
<h2>🍪 {{ __('portal.cookie.banner_title') }}</h2>
<p class="ck-intro">
{{ __('portal.cookie.banner_body') }}
<a href="{{ route('cookie.policy') }}" style="color:#93c5fd;text-decoration:underline;margin-left:4px;" target="_blank" rel="noopener">
{{ __('portal.cookie.read_policy') }}
</a>
<a href="{{ route('cookie.policy') }}" class="ck-link" target="_blank" rel="noopener">{{ __('portal.cookie.read_policy') }} →</a>
</p>
<div class="ck-btns">
<button class="ck-btn ck-btn-primary" @click="acceptAll()">{{ __('portal.cookie.accept') }}</button>
<button class="ck-btn ck-btn-secondary" @click="showDetails=true">{{ __('portal.cookie.customize') }}</button>
<button class="ck-btn ck-btn-ghost" @click="rejectAll()">{{ __('portal.cookie.reject') }}</button>
</div>
<div style="display:flex;gap:8px;flex-shrink:0;flex-wrap:wrap;">
<button
@click="reject()"
style="
padding: 8px 16px;
border: 1px solid #6b7280;
border-radius: 6px;
background: transparent;
color: #d1d5db;
cursor: pointer;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
"
>{{ __('portal.cookie.reject') }}</button>
<button
@click="accept()"
style="
padding: 8px 20px;
border: none;
border-radius: 6px;
background: {{ $themeColor ?? '#3B82F6' }};
color: #fff;
cursor: pointer;
font-size: 13px;
font-weight: 600;
white-space: nowrap;
"
>{{ __('portal.cookie.accept') }}</button>
</div>
<div x-show="showDetails" x-cloak>
{{-- Detailed categories view --}}
<h2>{{ __('portal.cookie.settings_title') }}</h2>
<p class="ck-intro">{{ __('portal.cookie.settings_body') }}</p>
{{-- Necessary --}}
<div class="ck-cat">
<div class="ck-cat-head">
<label>{{ __('portal.cookie.cat_necessary') }}</label>
<span class="ck-badge always">{{ __('portal.cookie.always_on') }}</span>
<label class="ck-toggle">
<input type="checkbox" checked disabled>
<span class="ck-slider"></span>
</label>
</div>
<p>{{ __('portal.cookie.cat_necessary_desc') }}</p>
</div>
{{-- Functional --}}
<div class="ck-cat">
<div class="ck-cat-head">
<label for="ck_func">{{ __('portal.cookie.cat_functional') }}</label>
<label class="ck-toggle">
<input id="ck_func" type="checkbox" x-model="consent.functional">
<span class="ck-slider"></span>
</label>
</div>
<p>{{ __('portal.cookie.cat_functional_desc') }}</p>
</div>
{{-- Analytics --}}
<div class="ck-cat">
<div class="ck-cat-head">
<label for="ck_anal">{{ __('portal.cookie.cat_analytics') }}</label>
<label class="ck-toggle">
<input id="ck_anal" type="checkbox" x-model="consent.analytics">
<span class="ck-slider"></span>
</label>
</div>
<p>{{ __('portal.cookie.cat_analytics_desc') }}</p>
</div>
{{-- Marketing --}}
<div class="ck-cat">
<div class="ck-cat-head">
<label for="ck_mktg">{{ __('portal.cookie.cat_marketing') }}</label>
<label class="ck-toggle">
<input id="ck_mktg" type="checkbox" x-model="consent.marketing">
<span class="ck-slider"></span>
</label>
</div>
<p>{{ __('portal.cookie.cat_marketing_desc') }}</p>
</div>
<div class="ck-btns">
<button class="ck-btn ck-btn-primary" @click="saveSettings()">{{ __('portal.cookie.save_settings') }}</button>
<button class="ck-btn ck-btn-secondary" @click="acceptAll()">{{ __('portal.cookie.accept') }}</button>
</div>
<div class="ck-footer">
<a href="{{ route('cookie.policy') }}" class="ck-link" target="_blank" rel="noopener">{{ __('portal.cookie.read_policy') }}</a>
<button class="ck-btn-ghost" style="border:none;background:transparent;font-size:12px;color:#6b7280;cursor:pointer;" @click="showDetails=false">← {{ __('portal.cookie.back') }}</button>
</div>
</div>
</div>
</div>
{{-- Floating re-open button (shown after consent given) --}}
<button class="ck-fab" x-show="!open && resolved" x-cloak @click="open=true;showDetails=true" title="{{ __('portal.cookie.manage_title') }}">🍪</button>
</div>
<script>
function cookieConsent() {
return {
open: false,
resolved: false,
showDetails: false,
consent: { necessary: true, functional: true, analytics: false, marketing: false },
init() {
const saved = localStorage.getItem('cookie_consent_v2');
if (saved) {
try {
const data = JSON.parse(saved);
this.consent = { necessary: true, ...data };
this.resolved = true;
this._apply();
} catch(e) {
this.open = true;
}
} else {
// Small delay so page content loads first
setTimeout(() => { this.open = true; }, 600);
}
},
acceptAll() {
this.consent = { necessary: true, functional: true, analytics: true, marketing: true };
this._save();
},
rejectAll() {
this.consent = { necessary: true, functional: false, analytics: false, marketing: false };
this._save();
},
saveSettings() {
this.consent.necessary = true;
this._save();
this.showDetails = false;
},
_save() {
localStorage.setItem('cookie_consent_v2', JSON.stringify(this.consent));
this.resolved = true;
this.open = false;
this._apply();
},
_apply() {
window.cookieConsent = { ...this.consent };
window.dispatchEvent(new CustomEvent('cookie-consent-updated', { detail: { ...this.consent } }));
// Load deferred analytics/marketing scripts based on consent
if (this.consent.analytics) {
document.dispatchEvent(new CustomEvent('cookie-analytics-granted'));
}
if (this.consent.marketing) {
document.dispatchEvent(new CustomEvent('cookie-marketing-granted'));
}
}
};
}
</script>
@endif
+31 -6
View File
@@ -13,13 +13,16 @@
header { background: {{ $themeColor }}; color: #fff; padding: 28px 16px; text-align: center; }
header img { max-height: 48px; margin-bottom: 10px; display: block; margin-inline: auto; }
header h1 { font-size: 20px; font-weight: 700; }
.wrap { max-width: 760px; margin: 0 auto; padding: 24px 16px 64px; }
.wrap { max-width: 760px; margin: 0 auto; padding: 24px 16px 80px; }
.card { background: #fff; border-radius: 12px; padding: 28px 24px; margin-top: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.05); }
.card h2 { font-size: 15px; font-weight: 700; margin-bottom: 12px; color: {{ $themeColor }}; letter-spacing: .02em; }
.policy-text { font-size: 14px; white-space: pre-wrap; color: #374151; line-height: 1.7; }
.card-title { font-size: 16px; font-weight: 700; margin-bottom: 14px; color: {{ $themeColor }}; }
.policy-text { font-size: 13.5px; white-space: pre-wrap; color: #374151; line-height: 1.75; }
.back-link { display: inline-flex; align-items: center; gap: 6px; color: {{ $themeColor }}; text-decoration: none; font-size: 14px; font-weight: 500; margin-bottom: 8px; }
.back-link:hover { opacity: .8; }
.manage-btn { display: inline-flex; align-items: center; gap: 6px; background: {{ $themeColor }}; color: #fff; border: none; padding: 10px 20px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; text-decoration: none; }
.manage-btn:hover { opacity: .9; }
footer { text-align: center; padding: 24px 16px; color: #9ca3af; font-size: 12px; }
[x-cloak] { display: none !important; }
@media (prefers-color-scheme: dark) {
body { background: #0f172a; color: #e5e7eb; }
.card { background: #1e293b; box-shadow: none; }
@@ -27,6 +30,7 @@
footer { color: #64748b; }
}
</style>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body>
<header>
@@ -37,19 +41,40 @@
</header>
<div class="wrap">
<a href="{{ url()->previous('/') }}" class="back-link">
<a href="javascript:history.back()" class="back-link">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24">
<path stroke-linecap="round" d="M15 19l-7-7 7-7"/>
</svg>
{{ __('portal.cookie.back') }}
</a>
<div class="card">
<h2>{{ __('portal.cookie.policy_title') }}</h2>
<div class="card" style="margin-bottom:12px;">
<p class="card-title">{{ __('portal.cookie.policy_title') }}</p>
<p class="policy-text">{{ $policyText }}</p>
</div>
<div class="card" style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;">
<div>
<p style="font-size:14px;font-weight:600;color:#111827;margin-bottom:2px;">{{ __('portal.cookie.manage_title') }}</p>
<p style="font-size:13px;color:#6b7280;">{{ __('portal.cookie.settings_body') }}</p>
</div>
<button class="manage-btn" onclick="openCookieSettings()">
🍪 {{ __('portal.cookie.customize') }}
</button>
</div>
<footer>{{ __('portal.common.powered_by') }}</footer>
</div>
@php $themeColor = $themeColor ?? '#3B82F6'; @endphp
@include('partials.cookie-banner')
<script>
function openCookieSettings() {
// Reset consent so the banner re-appears with settings panel open
localStorage.removeItem('cookie_consent_v2');
location.reload();
}
</script>
</body>
</html>
-1
View File
@@ -231,7 +231,6 @@
</footer>
</div>
@php $themeColor = $tenant->settings['theme_color'] ?? '#3B82F6'; @endphp
@include('partials.cookie-banner')
</body>
</html>