feat: cookie policy & consent banner per Moldova law 195/2024
- Settings page: new "Politica Cookie" section — toggle banner on/off, editable RO/RU policy text (defaults to full 14-section template) - Public route GET /cookie-policy → CookiePolicyController returns locale-aware full policy page with tenant branding - Cookie banner partial (Alpine.js, localStorage) shown on tracking and landing pages; Accept/Reject stores consent, banner dismisses - Translation keys added to lang/ro/portal.php and lang/ru/portal.php - Alpine.js (CDN) added to tracking and landing pages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
{{-- Cookie consent banner — localStorage-based, shown once per browser --}}
|
||||
@php
|
||||
$bannerEnabled = data_get($tenant->settings ?? [], 'cookie.banner_enabled', true);
|
||||
@endphp
|
||||
|
||||
@if ($bannerEnabled)
|
||||
<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;
|
||||
"
|
||||
>
|
||||
<div style="flex:1;min-width:200px;">
|
||||
<span style="font-weight:600;margin-right:6px;">🍪 {{ __('portal.cookie.banner_title') }}</span>
|
||||
{{ __('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>
|
||||
</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>
|
||||
@endif
|
||||
@@ -0,0 +1,55 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ $locale }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>{{ __('portal.cookie.policy_title') }} — {{ $tenant?->display_name ?? $tenant?->name ?? config('app.name') }}</title>
|
||||
@if ($faviconUrl ?? null)
|
||||
<link rel="icon" href="{{ $faviconUrl }}">
|
||||
@endif
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; color: #1f2937; background: #f3f4f6; line-height: 1.6; }
|
||||
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; }
|
||||
.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; }
|
||||
.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; }
|
||||
footer { text-align: center; padding: 24px 16px; color: #9ca3af; font-size: 12px; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body { background: #0f172a; color: #e5e7eb; }
|
||||
.card { background: #1e293b; box-shadow: none; }
|
||||
.policy-text { color: #cbd5e1; }
|
||||
footer { color: #64748b; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
@if ($logoUrl ?? null)
|
||||
<img src="{{ $logoUrl }}" alt="">
|
||||
@endif
|
||||
<h1>{{ $tenant?->display_name ?? $tenant?->name }}</h1>
|
||||
</header>
|
||||
|
||||
<div class="wrap">
|
||||
<a href="{{ url()->previous('/') }}" 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>
|
||||
<p class="policy-text">{{ $policyText }}</p>
|
||||
</div>
|
||||
|
||||
<footer>{{ __('portal.common.powered_by') }}</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -48,7 +48,9 @@
|
||||
|
||||
footer { background: #1f2937; color: #9ca3af; padding: 24px; text-align: center; font-size: 13px; }
|
||||
footer a { color: #d1d5db; text-decoration: none; }
|
||||
[x-cloak] { display: none !important; }
|
||||
</style>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -116,5 +118,9 @@
|
||||
© {{ date('Y') }} {{ $name }} · powered by AutoCRM
|
||||
</footer>
|
||||
|
||||
@php
|
||||
$tenant = app(\App\Tenancy\TenantManager::class)->current();
|
||||
@endphp
|
||||
@include('partials.cookie-banner')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
.totals .amt { font-size: 22px; font-weight: 700; color: {{ $themeColor }}; }
|
||||
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; }
|
||||
@@ -80,6 +81,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 style="position:relative;">
|
||||
@@ -228,5 +230,8 @@
|
||||
{{ __('portal.common.powered_by') }}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@php $themeColor = $tenant->settings['theme_color'] ?? '#3B82F6'; @endphp
|
||||
@include('partials.cookie-banner')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user