Fixed log mobile view, plot, privacy banner
Build and Publish / Build and Publish Docker Image (push) Successful in 1m42s

This commit is contained in:
ab
2026-09-20 21:00:03 +03:00
parent 9587622285
commit 6449d8b59a
9 changed files with 295 additions and 47 deletions
+3 -3
View File
@@ -1585,8 +1585,8 @@ async fn analytics_events(
let mut out = Vec::with_capacity(page.len()); let mut out = Vec::with_capacity(page.len());
for e in &page { for e in &page {
let client_name = e.client_id.as_ref().map(|fk| { let client_id = e.client_id.as_ref().map(|fk| fk.primary_key().unwrap());
let cid = fk.primary_key().unwrap(); let client_name = client_id.map(|cid| {
clients clients
.iter() .iter()
.find(|c| c.id.unwrap() == cid) .find(|c| c.id.unwrap() == cid)
@@ -1602,12 +1602,12 @@ async fn analytics_events(
"id": e.id.unwrap(), "id": e.id.unwrap(),
"created_at": local.format("%Y-%m-%d %H:%M").to_string(), "created_at": local.format("%Y-%m-%d %H:%M").to_string(),
"event_type": e.event_type, "event_type": e.event_type,
"client_id": client_id,
"client_name": client_name, "client_name": client_name,
"visitor": e.visitor_hash.chars().take(8).collect::<String>(), "visitor": e.visitor_hash.chars().take(8).collect::<String>(),
"media": media_ref, "media": media_ref,
"user_agent": e.user_agent, "user_agent": e.user_agent,
"referer": e.referer, "referer": e.referer,
"path": e.path,
})); }));
} }
+28
View File
@@ -223,6 +223,10 @@ pub struct Translations {
pub analytics_ev_landing_view: &'static str, pub analytics_ev_landing_view: &'static str,
pub analytics_ev_portal_open: &'static str, pub analytics_ev_portal_open: &'static str,
pub analytics_ev_media_view: &'static str, pub analytics_ev_media_view: &'static str,
pub analytics_ev_landing_short: &'static str,
pub analytics_ev_portal_short: &'static str,
pub analytics_ev_media_short: &'static str,
pub analytics_log_media_prefix: &'static str,
// Dashboard // Dashboard
pub dashboard_title: &'static str, pub dashboard_title: &'static str,
@@ -281,6 +285,12 @@ pub struct Translations {
pub landing_footer_text: &'static str, pub landing_footer_text: &'static str,
pub landing_footer_copyright: &'static str, pub landing_footer_copyright: &'static str,
// Cookies & privacy
pub cookie_banner_text: &'static str,
pub cookie_banner_accept: &'static str,
pub cookie_privacy_link: &'static str,
pub privacy_title: &'static str,
// Testimonials admin // Testimonials admin
pub nav_testimonials: &'static str, pub nav_testimonials: &'static str,
pub testimonials_title: &'static str, pub testimonials_title: &'static str,
@@ -545,6 +555,10 @@ static RU: Translations = Translations {
analytics_ev_landing_view: "Заход на сайт", analytics_ev_landing_view: "Заход на сайт",
analytics_ev_portal_open: "Открытие страницы клиента", analytics_ev_portal_open: "Открытие страницы клиента",
analytics_ev_media_view: "Просмотр фото", analytics_ev_media_view: "Просмотр фото",
analytics_ev_landing_short: "Сайт",
analytics_ev_portal_short: "Портал",
analytics_ev_media_short: "Фото",
analytics_log_media_prefix: "Просмотр медиа",
dashboard_title: "Главная", dashboard_title: "Главная",
dashboard_today_visits: "Визиты на сегодня", dashboard_today_visits: "Визиты на сегодня",
@@ -680,6 +694,11 @@ static RU: Translations = Translations {
landing_footer_text: "МурНяня.РФ — Присмотрим, погладим, покормим", landing_footer_text: "МурНяня.РФ — Присмотрим, погладим, покормим",
landing_footer_copyright: "Все права защищены", landing_footer_copyright: "Все права защищены",
cookie_banner_text: "Мы используем файлы cookie для работы сайта и анонимной статистики посещений.",
cookie_banner_accept: "Принять",
cookie_privacy_link: "Политика конфиденциальности",
privacy_title: "Политика конфиденциальности и использования cookie",
nav_testimonials: "Отзывы", nav_testimonials: "Отзывы",
testimonials_title: "Отзывы", testimonials_title: "Отзывы",
testimonials_empty: "Отзывов пока нет.", testimonials_empty: "Отзывов пока нет.",
@@ -857,6 +876,10 @@ static EN: Translations = Translations {
analytics_ev_landing_view: "Landing visit", analytics_ev_landing_view: "Landing visit",
analytics_ev_portal_open: "Client portal open", analytics_ev_portal_open: "Client portal open",
analytics_ev_media_view: "Photo view", analytics_ev_media_view: "Photo view",
analytics_ev_landing_short: "Site",
analytics_ev_portal_short: "Portal",
analytics_ev_media_short: "Photos",
analytics_log_media_prefix: "Media view",
dashboard_title: "Home", dashboard_title: "Home",
dashboard_today_visits: "Today's visits", dashboard_today_visits: "Today's visits",
@@ -992,6 +1015,11 @@ static EN: Translations = Translations {
landing_footer_text: "МурНяня.РФ — Присмотрим, погладим, покормим", landing_footer_text: "МурНяня.РФ — Присмотрим, погладим, покормим",
landing_footer_copyright: "All rights reserved", landing_footer_copyright: "All rights reserved",
cookie_banner_text: "We use cookies to run the site and for anonymous visit statistics.",
cookie_banner_accept: "Accept",
cookie_privacy_link: "Privacy Policy",
privacy_title: "Privacy & Cookie Policy",
nav_testimonials: "Testimonials", nav_testimonials: "Testimonials",
testimonials_title: "Testimonials", testimonials_title: "Testimonials",
testimonials_empty: "No testimonials yet.", testimonials_empty: "No testimonials yet.",
+14
View File
@@ -105,6 +105,19 @@ struct ThankYouTemplate<'a> {
lang: Lang, lang: Lang,
} }
#[derive(Debug, Template)]
#[template(path = "privacy.html")]
struct PrivacyTemplate<'a> {
t: &'a Translations,
lang: Lang,
}
async fn privacy_page(request: Request) -> cot::Result<Response> {
let lang = detect_lang(&request);
let body = PrivacyTemplate { t: lang.t(), lang }.render()?;
html_response(body, lang)
}
async fn landing_page(request: Request, db: Database) -> cot::Result<Response> { async fn landing_page(request: Request, db: Database) -> cot::Result<Response> {
let lang = detect_lang(&request); let lang = detect_lang(&request);
@@ -1235,6 +1248,7 @@ pub fn public_router() -> Router {
Route::with_handler_and_name("/static/{filename}", serve_static, "static-file"), Route::with_handler_and_name("/static/{filename}", serve_static, "static-file"),
Route::with_handler_and_name("/robots.txt", robots_txt, "robots-txt"), Route::with_handler_and_name("/robots.txt", robots_txt, "robots-txt"),
Route::with_handler_and_name("/sitemap.xml", sitemap_xml, "sitemap-xml"), Route::with_handler_and_name("/sitemap.xml", sitemap_xml, "sitemap-xml"),
Route::with_handler_and_name("/privacy", privacy_page, "privacy"),
Route::with_handler_and_name("/submit", submit_lead, "submit-lead"), Route::with_handler_and_name("/submit", submit_lead, "submit-lead"),
Route::with_handler_and_name( Route::with_handler_and_name(
"/testimonial-image/{id}", "/testimonial-image/{id}",
+103 -44
View File
@@ -25,7 +25,8 @@
"visitor": "{{ t.analytics_log_visitor }}", "visitor": "{{ t.analytics_log_visitor }}",
"landing_view": "{{ t.analytics_ev_landing_view }}", "landing_view": "{{ t.analytics_ev_landing_view }}",
"portal_open": "{{ t.analytics_ev_portal_open }}", "portal_open": "{{ t.analytics_ev_portal_open }}",
"media_view": "{{ t.analytics_ev_media_view }}" "media_view": "{{ t.analytics_ev_media_view }}",
"media_prefix": "{{ t.analytics_log_media_prefix }}"
}'> }'>
<div class="analytics-presets"> <div class="analytics-presets">
<button type="button" class="button is-small preset-btn" data-days="7">{{ t.analytics_preset_7 }}</button> <button type="button" class="button is-small preset-btn" data-days="7">{{ t.analytics_preset_7 }}</button>
@@ -52,14 +53,6 @@
<div class="analytics-kpis" id="anKpis"></div> <div class="analytics-kpis" id="anKpis"></div>
<div class="analytics-metric-toggles">
<span class="toggles-label">{{ t.analytics_metrics_label }}:</span>
<label><input type="checkbox" class="metric-toggle" data-key="landing_views"> {{ t.analytics_metric_landing_views }}</label>
<label><input type="checkbox" class="metric-toggle" data-key="landing_unique" checked> {{ t.analytics_metric_landing_unique }}</label>
<label><input type="checkbox" class="metric-toggle" data-key="portal_opens" checked> {{ t.analytics_metric_portal_opens }}</label>
<label><input type="checkbox" class="metric-toggle" data-key="media_views" checked> {{ t.analytics_metric_media_views }}</label>
</div>
<div class="analytics-chart-wrap"> <div class="analytics-chart-wrap">
<div class="analytics-chart-box"> <div class="analytics-chart-box">
<canvas id="anChart"></canvas> <canvas id="anChart"></canvas>
@@ -86,11 +79,19 @@
<div class="analytics-log-card"> <div class="analytics-log-card">
<div class="analytics-log-head"> <div class="analytics-log-head">
<h2>{{ t.analytics_log_title }}</h2> <h2>{{ t.analytics_log_title }}</h2>
<div class="analytics-metric-toggles analytics-log-filters"> <div class="analytics-log-filters">
<span class="toggles-label">{{ t.analytics_log_show }}:</span> <label class="log-chip" title="{{ t.analytics_ev_landing_view }}">
<label><input type="checkbox" class="log-type-toggle" data-type="landing_view"> {{ t.analytics_ev_landing_view }}</label> <input type="checkbox" class="log-type-toggle" data-type="landing_view">
<label><input type="checkbox" class="log-type-toggle" data-type="portal_open" checked> {{ t.analytics_ev_portal_open }}</label> <span>🌐 {{ t.analytics_ev_landing_short }}</span>
<label><input type="checkbox" class="log-type-toggle" data-type="media_view" checked> {{ t.analytics_ev_media_view }}</label> </label>
<label class="log-chip" title="{{ t.analytics_ev_portal_open }}">
<input type="checkbox" class="log-type-toggle" data-type="portal_open" checked>
<span>👤 {{ t.analytics_ev_portal_short }}</span>
</label>
<label class="log-chip" title="{{ t.analytics_ev_media_view }}">
<input type="checkbox" class="log-type-toggle" data-type="media_view" checked>
<span>🖼️ {{ t.analytics_ev_media_short }}</span>
</label>
</div> </div>
</div> </div>
<div class="analytics-log" id="anLog"></div> <div class="analytics-log" id="anLog"></div>
@@ -127,7 +128,14 @@
.analytics-log-card { background:#fff; border-radius:12px; padding:16px; box-shadow:0 1px 4px rgba(0,0,0,.06); margin-top:20px; } .analytics-log-card { background:#fff; border-radius:12px; padding:16px; box-shadow:0 1px 4px rgba(0,0,0,.06); margin-top:20px; }
.analytics-log-card h2 { font-size:1rem; margin:0; } .analytics-log-card h2 { font-size:1rem; margin:0; }
.analytics-log-head { display:flex; flex-wrap:wrap; gap:10px 18px; align-items:center; justify-content:space-between; margin-bottom:10px; } .analytics-log-head { display:flex; flex-wrap:wrap; gap:10px 18px; align-items:center; justify-content:space-between; margin-bottom:10px; }
.analytics-log-filters { margin-bottom:0; } .analytics-log-filters { display:flex; flex-wrap:wrap; gap:6px; }
.log-chip { display:inline-flex; align-items:center; gap:5px; padding:4px 11px; border-radius:999px; border:1px solid var(--admin-border,#e0e0e6); background:#f5f5f7; color:#8a8a95; font-size:.8rem; line-height:1.3; cursor:pointer; user-select:none; white-space:nowrap; transition:background .12s,color .12s,border-color .12s; }
.log-chip input { position:absolute; opacity:0; width:0; height:0; pointer-events:none; }
.log-chip:has(input:checked) { background:var(--admin-accent-soft,#eef0fb); border-color:#c9c6ee; color:#5b4fc4; font-weight:600; }
@media (max-width:600px) {
.analytics-log-head { gap:8px; margin-bottom:8px; }
.log-chip { padding:5px 12px; font-size:.82rem; }
}
.analytics-log { height:460px; overflow-y:auto; overflow-x:hidden; border:1px solid #f0f0f0; border-radius:8px; } .analytics-log { height:460px; overflow-y:auto; overflow-x:hidden; border:1px solid #f0f0f0; border-radius:8px; }
.an-log-row { display:grid; grid-template-columns:40px 1fr; gap:10px; align-items:start; padding:11px 12px; border-bottom:1px solid #f2f2f4; font-size:.85rem; } .an-log-row { display:grid; grid-template-columns:40px 1fr; gap:10px; align-items:start; padding:11px 12px; border-bottom:1px solid #f2f2f4; font-size:.85rem; }
.an-log-row:last-child { border-bottom:none; } .an-log-row:last-child { border-bottom:none; }
@@ -136,7 +144,10 @@
.an-log-main { min-width:0; } .an-log-main { min-width:0; }
.an-log-head { display:flex; flex-wrap:wrap; gap:2px 8px; align-items:baseline; } .an-log-head { display:flex; flex-wrap:wrap; gap:2px 8px; align-items:baseline; }
.an-log-event { font-weight:650; } .an-log-event { font-weight:650; }
.an-log-client { color:#7c6ed4; font-weight:600; } .an-log-client { color:#7c6ed4; font-weight:600; text-decoration:none; }
.an-log-client:hover { text-decoration:underline; }
.an-log-num { color:#7c6ed4; font-weight:600; text-decoration:none; }
a.an-log-num:hover { text-decoration:underline; }
.an-log-time { color:#9a9aa6; font-size:.75rem; white-space:nowrap; margin-left:auto; } .an-log-time { color:#9a9aa6; font-size:.75rem; white-space:nowrap; margin-left:auto; }
.an-log-meta { color:#8a8a95; font-size:.76rem; margin-top:4px; display:flex; flex-direction:column; gap:2px; } .an-log-meta { color:#8a8a95; font-size:.76rem; margin-top:4px; display:flex; flex-direction:column; gap:2px; }
.an-log-meta .an-log-line { display:flex; flex-wrap:wrap; gap:4px 12px; } .an-log-meta .an-log-line { display:flex; flex-wrap:wrap; gap:4px 12px; }
@@ -157,7 +168,7 @@
var L = JSON.parse(controls.getAttribute('data-labels')); var L = JSON.parse(controls.getAttribute('data-labels'));
var META = [ var META = [
{ key: 'landing_views', color: '#7c6ed4' }, { key: 'landing_views', color: '#7c6ed4', hidden: true },
{ key: 'landing_unique', color: '#00b496' }, { key: 'landing_unique', color: '#00b496' },
{ key: 'portal_opens', color: '#ff8c26' }, { key: 'portal_opens', color: '#ff8c26' },
{ key: 'media_views', color: '#ff5287' } { key: 'media_views', color: '#ff5287' }
@@ -212,6 +223,23 @@
function metricLabel(key) { return L[key] || key; } function metricLabel(key) { return L[key] || key; }
var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
// Turns an ISO bucket label into a short display date:
// "2026-04-11" -> "11 Apr", "2026-04" -> "Apr 2026".
function formatLabel(raw) {
if (!raw) { return raw; }
var parts = String(raw).split('-');
if (parts.length === 3) {
return parseInt(parts[2], 10) + ' ' + MONTHS[parseInt(parts[1], 10) - 1];
}
if (parts.length === 2) {
return MONTHS[parseInt(parts[1], 10) - 1] + ' ' + parts[0];
}
return raw;
}
function hexToRgba(hex, a) { function hexToRgba(hex, a) {
var n = parseInt(hex.slice(1), 16); var n = parseInt(hex.slice(1), 16);
return 'rgba(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + a + ')'; return 'rgba(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + a + ')';
@@ -239,10 +267,6 @@
function initChart() { function initChart() {
var ctx = document.getElementById('anChart').getContext('2d'); var ctx = document.getElementById('anChart').getContext('2d');
var enabled = {};
document.querySelectorAll('.metric-toggle').forEach(function (cb) {
enabled[cb.getAttribute('data-key')] = cb.checked;
});
var datasets = META.map(function (m) { var datasets = META.map(function (m) {
return { return {
label: metricLabel(m.key), label: metricLabel(m.key),
@@ -253,7 +277,7 @@
pointRadius: 2, pointRadius: 2,
tension: 0.25, tension: 0.25,
fill: true, fill: true,
hidden: enabled[m.key] === false, hidden: !!m.hidden,
_key: m.key _key: m.key
}; };
}); });
@@ -265,8 +289,39 @@
maintainAspectRatio: false, maintainAspectRatio: false,
animation: false, animation: false,
interaction: { mode: 'index', intersect: false }, interaction: { mode: 'index', intersect: false },
plugins: { legend: { display: false } }, plugins: {
scales: { y: { beginAtZero: true, ticks: { precision: 0 } } } legend: {
display: true,
position: 'top',
labels: {
usePointStyle: true,
pointStyle: 'rectRounded',
boxWidth: 12,
boxHeight: 12,
padding: 12,
font: { size: 12 }
}
},
tooltip: {
callbacks: {
title: function (items) {
return items.length ? formatLabel(items[0].label) : '';
}
}
}
},
scales: {
x: {
ticks: {
autoSkip: true,
maxRotation: 0,
callback: function (value) {
return formatLabel(this.getLabelForValue(value));
}
}
},
y: { beginAtZero: true, ticks: { precision: 0 } }
}
}, },
plugins: [messageOverlay] plugins: [messageOverlay]
}); });
@@ -401,15 +456,6 @@
fromInput.addEventListener('change', load); fromInput.addEventListener('change', load);
toInput.addEventListener('change', load); toInput.addEventListener('change', load);
granSelect.addEventListener('change', load); granSelect.addEventListener('change', load);
document.querySelectorAll('.metric-toggle').forEach(function (cb) {
cb.addEventListener('change', function () {
if (!chart) { return; }
chart.data.datasets.forEach(function (ds) {
if (ds._key === cb.getAttribute('data-key')) { ds.hidden = !cb.checked; }
});
chart.update();
});
});
// --- Event log (newest first, infinite scroll of older events) --------- // --- Event log (newest first, infinite scroll of older events) ---------
var logEl = document.getElementById('anLog'); var logEl = document.getElementById('anLog');
@@ -487,16 +533,37 @@
var head = document.createElement('div'); var head = document.createElement('div');
head.className = 'an-log-head'; head.className = 'an-log-head';
var ev = document.createElement('span'); var ev = document.createElement('span');
ev.className = 'an-log-event'; ev.className = 'an-log-event';
ev.textContent = L[e.event_type] || e.event_type; if (e.event_type === 'media_view') {
head.appendChild(ev); ev.textContent = L.media_prefix;
head.appendChild(ev);
if (e.media && e.media.media_id != null) {
var num = document.createElement(e.client_id != null ? 'a' : 'span');
num.className = 'an-log-num';
num.textContent = '№' + e.media.media_id;
if (e.client_id != null) { num.href = '/admin/media?client_id=' + e.client_id; }
head.appendChild(num);
}
} else {
ev.textContent = L[e.event_type] || e.event_type;
head.appendChild(ev);
}
if (e.client_name) { if (e.client_name) {
var cl = document.createElement('span'); var cl;
if (e.client_id != null) {
cl = document.createElement('a');
cl.href = '/admin/clients/' + e.client_id + '/edit';
} else {
cl = document.createElement('span');
}
cl.className = 'an-log-client'; cl.className = 'an-log-client';
cl.textContent = e.client_name; cl.textContent = e.client_name;
head.appendChild(cl); head.appendChild(cl);
} }
var tm = document.createElement('span'); var tm = document.createElement('span');
tm.className = 'an-log-time'; tm.className = 'an-log-time';
tm.textContent = e.created_at; tm.textContent = e.created_at;
@@ -512,14 +579,6 @@
if (ua) { line1.appendChild(metaSpan(ua)); } if (ua) { line1.appendChild(metaSpan(ua)); }
if (line1.childNodes.length) { meta.appendChild(line1); } if (line1.childNodes.length) { meta.appendChild(line1); }
if (e.path && e.path !== '/') {
var line2 = metaLine();
var code = document.createElement('code');
code.textContent = e.path;
line2.appendChild(code);
meta.appendChild(line2);
}
if (e.referer) { if (e.referer) {
var line3 = metaLine(); var line3 = metaLine();
var ref = document.createElement('span'); var ref = document.createElement('span');
+1
View File
@@ -749,5 +749,6 @@ document.addEventListener('keydown', function(event) {
}); });
</script> </script>
{% include "partials/lightbox.html" %} {% include "partials/lightbox.html" %}
{% include "partials/cookie_banner.html" %}
</body> </body>
</html> </html>
+1
View File
@@ -569,5 +569,6 @@
})(); })();
</script> </script>
{% include "partials/cookie_banner.html" %}
</body> </body>
</html> </html>
+47
View File
@@ -0,0 +1,47 @@
<div class="cookie-banner" id="cookieBanner">
<p class="cookie-banner-text">
{{ t.cookie_banner_text }}
<a href="/privacy?lang={{ lang.code() }}">{{ t.cookie_privacy_link }}</a>
</p>
<button type="button" class="cookie-banner-accept" id="cookieAccept">{{ t.cookie_banner_accept }}</button>
</div>
<style>
.cookie-banner {
display: none;
position: fixed; left: 0; right: 0; bottom: 0; z-index: 300;
flex-wrap: wrap; gap: .75rem 1.25rem; align-items: center; justify-content: center;
padding: .85rem 1.1rem;
background: rgba(26, 26, 46, .95); color: #fff;
box-shadow: 0 -4px 24px rgba(0, 0, 0, .18);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.cookie-banner.is-open { display: flex; }
.cookie-banner-text { margin: 0; font-size: .88rem; line-height: 1.45; max-width: 720px; }
.cookie-banner-text a { color: #b9b2ff; text-decoration: underline; }
.cookie-banner-accept {
flex: 0 0 auto; border: none; cursor: pointer;
padding: .6rem 1.6rem; border-radius: 10px;
background: #6c63ff; color: #fff; font-weight: 700; font-size: .9rem;
transition: background .18s, transform .18s;
}
.cookie-banner-accept:hover { background: #5a52d5; transform: translateY(-1px); }
@media (max-width: 600px) {
.cookie-banner.is-open { flex-direction: column; text-align: center; padding: 1rem; gap: .7rem; }
.cookie-banner-accept { width: 100%; }
}
</style>
<script>
(function () {
var KEY = 'cookie_consent';
var banner = document.getElementById('cookieBanner');
if (!banner) { return; }
var accepted;
try { accepted = window.localStorage.getItem(KEY) === '1'; } catch (e) { accepted = false; }
if (accepted) { return; }
banner.classList.add('is-open');
document.getElementById('cookieAccept').addEventListener('click', function () {
try { window.localStorage.setItem(KEY, '1'); } catch (e) {}
banner.classList.remove('is-open');
});
})();
</script>
+97
View File
@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="{{ lang.code() }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex">
<title>{{ t.nav_title }} — {{ t.privacy_title }}</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style>
:root { color-scheme: light; }
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
color: #1a1a2e; background: linear-gradient(170deg, #f0eeff 0%, #fff 60%);
min-height: 100vh; padding: 2rem 1rem;
}
.doc {
max-width: 760px; margin: 0 auto; background: #fff; border-radius: 18px;
border: 1px solid #e8e6ff; box-shadow: 0 8px 40px rgba(108,99,255,0.08);
padding: 2.4rem 2rem;
}
h1 { font-size: 1.6rem; font-weight: 800; margin-bottom: 1.2rem; line-height: 1.25; }
h2 { font-size: 1.1rem; font-weight: 700; margin: 1.6rem 0 .5rem; }
p, li { color: #444; font-size: .98rem; line-height: 1.6; }
ul { padding-left: 1.3rem; margin: .3rem 0; }
li { margin: .25rem 0; }
.muted { color: #888; font-size: .85rem; margin-top: 1.8rem; }
.back-link {
display: inline-block; margin-top: 1.8rem; padding: .7rem 1.8rem; border-radius: 12px;
background: #6c63ff; color: #fff; font-weight: 700; text-decoration: none;
transition: background .2s, transform .2s;
}
.back-link:hover { background: #5a52d5; transform: translateY(-1px); }
</style>
</head>
<body>
<div class="doc">
<h1>{{ t.privacy_title }}</h1>
{% if lang.code() == "ru" %}
<p>Настоящая политика описывает, какие данные обрабатывает сайт «{{ t.nav_title }}» и как используются файлы cookie. Продолжая пользоваться сайтом, вы соглашаетесь с условиями, изложенными ниже.</p>
<h2>Какие данные мы собираем</h2>
<ul>
<li><strong>Данные из формы заявки</strong> — имя, телефон и комментарий, которые вы указываете добровольно, чтобы мы могли связаться с вами по поводу услуги.</li>
<li><strong>Файлы cookie и техническая информация</strong> — см. раздел ниже. IP-адреса мы не храним.</li>
</ul>
<h2>Какие файлы cookie используются</h2>
<ul>
<li><strong>Языковая настройка</strong> — запоминает выбранный язык интерфейса.</li>
<li><strong>Сессия</strong> — необходима для входа администратора в панель управления.</li>
<li><strong>Статистика посещений</strong> — анонимный идентификатор, который помогает считать число уникальных посещений и просмотров. Он не содержит персональных данных, не передаётся третьим лицам и не используется для рекламы.</li>
</ul>
<h2>Цели обработки</h2>
<p>Связь с вами по оставленной заявке, оказание услуг по уходу за животными, а также улучшение работы сайта на основе обезличенной статистики.</p>
<h2>Передача третьим лицам</h2>
<p>Мы не продаём и не передаём ваши данные третьим лицам, за исключением случаев, предусмотренных законодательством.</p>
<h2>Ваши права</h2>
<p>Вы можете запросить сведения об обработке ваших данных, их изменение или удаление, а также отозвать согласие, связавшись с нами по контактам, указанным на сайте. Файлы cookie можно отключить в настройках вашего браузера.</p>
<p class="muted">Дата последнего обновления: 2026. Оператор может уточнить реквизиты и контактные данные в этом документе.</p>
{% else %}
<p>This policy explains what data the “{{ t.nav_title }}” website processes and how cookies are used. By continuing to use the site, you agree to the terms set out below.</p>
<h2>What data we collect</h2>
<ul>
<li><strong>Request form data</strong> — the name, phone number and comment you voluntarily provide so we can contact you about the service.</li>
<li><strong>Cookies and technical information</strong> — see the section below. We do not store IP addresses.</li>
</ul>
<h2>Cookies we use</h2>
<ul>
<li><strong>Language preference</strong> — remembers your selected interface language.</li>
<li><strong>Session</strong> — required for the administrator to sign in to the control panel.</li>
<li><strong>Visit statistics</strong> — an anonymous identifier that helps count unique visits and views. It contains no personal data, is not shared with third parties, and is not used for advertising.</li>
</ul>
<h2>Purposes of processing</h2>
<p>Contacting you about your request, providing pet-sitting services, and improving the website based on anonymised statistics.</p>
<h2>Sharing with third parties</h2>
<p>We do not sell or share your data with third parties, except as required by law.</p>
<h2>Your rights</h2>
<p>You may request information about the processing of your data, its correction or deletion, and withdraw your consent by contacting us via the details provided on the site. Cookies can be disabled in your browser settings.</p>
<p class="muted">Last updated: 2026. The operator may add legal and contact details to this document.</p>
{% endif %}
<a href="/?lang={{ lang.code() }}" class="back-link">{{ t.landing_thank_you_back }}</a>
</div>
</body>
</html>
+1
View File
@@ -37,5 +37,6 @@
<p>{{ t.landing_thank_you_text }}</p> <p>{{ t.landing_thank_you_text }}</p>
<a href="/?lang={{ lang.code() }}" class="back-link">{{ t.landing_thank_you_back }}</a> <a href="/?lang={{ lang.code() }}" class="back-link">{{ t.landing_thank_you_back }}</a>
</div> </div>
{% include "partials/cookie_banner.html" %}
</body> </body>
</html> </html>