feat(rgpd): política de privacidade e termos, mapa a pedido, fotos locais, retenção, tema claro/escuro

- Política de privacidade reescrita: tabela de dados/finalidades/fundamento
  (inclui os pedidos de aula, inscrição e eventos com IP), prazos concretos,
  subcontratantes, Google Maps e reviews, cookies. Termos e condições novos
  (/home/termos), no rodapé e no sitemap. Campos [ENTRE PARÊNTESES RETOS]
  e revisão por jurista pendentes antes de publicar no domínio real.
- Google Maps só carrega ao carregar em «Mostrar mapa» (map_click_to_load);
  as fotos dos autores das reviews são guardadas pelo servidor em
  uploads/reviews/ e servidas localmente (review_avatar_url).
- Retenção que a política declara, aplicada pelo cron (04:00): leads com
  mais de 12 meses → lixeira; blocked_submissions e outbox de segurança
  apagados aos 90 dias (trash_retention_sweep, auditado).
- Backoffice e login com tema claro/escuro/automático: data-bs-theme
  aplicado no <head> antes do CSS (sem flash), escolha em localStorage,
  segue o sistema em tempo real, color-scheme e theme-color para os dois.
  Cores próprias passaram a variáveis do Bootstrap. lang="pt-PT".

Testado numa cópia da BD de produção: 72/72 verificações.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
andreandClaude Opus 5.5 committed 2026-09-25 11:39:05 +01:00
1 parent 04c8c83e66
commit b202262da0
16 files changed
+297 -34

No files matched your search

+19 -4
View File
@@ -8,12 +8,26 @@
$page_title = $page_title ?? 'Entrar';
$app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
?><!doctype html>
<html lang="pt">
<html lang="pt-PT">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title><?= htmlspecialchars($page_title) ?> — <?= htmlspecialchars($app_name) ?></title>
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#eef0f4" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0a0a0a" media="(prefers-color-scheme: dark)">
<script>
// Tema antes do CSS, para não piscar o tema errado. Escolha guardada no
// navegador (claro / escuro / auto); auto segue o sistema operativo.
(function () {
var pref = 'auto';
try { pref = localStorage.getItem('bo-theme') || 'auto'; } catch (e) {}
var dark = pref === 'dark' || (pref === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.setAttribute('data-bs-theme', dark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme-pref', pref);
})();
</script>
<link href="<?= base_url('assets/vendor/bootstrap5/css/bootstrap.min.css') ?>" rel="stylesheet">
<link href="<?= base_url('assets/vendor/bootstrap-icons/bootstrap-icons.css') ?>" rel="stylesheet">
<style>
@@ -32,7 +46,7 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
}
.auth-card {
width: 100%; max-width: 420px;
background: #fff; color: #0a0a0a;
background: var(--bs-body-bg); color: var(--bs-body-color);
border-radius: 14px; padding: 2rem;
box-shadow: 0 18px 50px rgba(0,0,0,.45);
}
@@ -41,12 +55,13 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
letter-spacing: .5px; margin-bottom: .25rem;
}
.auth-brand i { color: var(--bo-accent); }
.auth-sub { text-align: center; color: #6c757d; font-size: .875rem; margin-bottom: 1.5rem; }
.auth-sub { text-align: center; color: var(--bs-secondary-color); font-size: .875rem; margin-bottom: 1.5rem; }
.btn-primary { background: var(--bo-accent); border-color: var(--bo-accent); color: var(--bo-dark); font-weight: 600; }
.btn-primary:hover, .btn-primary:focus { background: #a8ba00; border-color: #a8ba00; color: var(--bo-dark); }
.form-control:focus { border-color: var(--bo-accent); box-shadow: 0 0 0 .2rem rgba(var(--bo-accent-rgb), .25); }
.auth-foot { color: #6c757d; }
.auth-foot { color: var(--bs-secondary-color); }
.auth-foot a, .auth-card a { color: #6a7a00; }
[data-bs-theme="dark"] .auth-foot a, [data-bs-theme="dark"] .auth-card a { color: var(--bo-accent); }
/* 16px inputs stop iOS from auto-zooming on focus */
@media (max-width: 575.98px) { .form-control { font-size: 16px; } .auth-card { padding: 1.5rem; } }
</style>
+33
View File
@@ -34,6 +34,39 @@ document.addEventListener('DOMContentLoaded', function () {
});
});
// Tema: claro / escuro / automático (o <head> já aplicou o guardado).
(function () {
var root = document.documentElement, btn = document.getElementById('themeToggle');
var mq = window.matchMedia('(prefers-color-scheme: dark)');
var icons = { light: 'bi-sun', dark: 'bi-moon-stars', auto: 'bi-circle-half' };
var names = { light: 'claro', dark: 'escuro', auto: 'automático' };
function apply(pref) {
var dark = pref === 'dark' || (pref === 'auto' && mq.matches);
root.setAttribute('data-bs-theme', dark ? 'dark' : 'light');
root.setAttribute('data-theme-pref', pref);
if (btn) {
btn.querySelector('i').className = 'bi ' + icons[pref];
btn.setAttribute('aria-label', 'Tema: ' + names[pref]);
}
document.querySelectorAll('[data-theme-set]').forEach(function (b) {
var on = b.getAttribute('data-theme-set') === pref;
b.classList.toggle('active', on);
b.setAttribute('aria-pressed', on ? 'true' : 'false');
});
}
var pref = root.getAttribute('data-theme-pref') || 'auto';
apply(pref);
document.querySelectorAll('[data-theme-set]').forEach(function (b) {
b.addEventListener('click', function () {
pref = b.getAttribute('data-theme-set');
try { localStorage.setItem('bo-theme', pref); } catch (e) {}
apply(pref);
});
});
// O sistema mudou de tema: só conta em «automático».
mq.addEventListener('change', function () { if (pref === 'auto') apply('auto'); });
})();
// Mobile off-canvas sidebar: hamburger toggles .sidebar.open + the backdrop;
// tapping the backdrop, a nav link, Escape, or resizing to desktop closes it.
(function () {
+34 -6
View File
@@ -6,11 +6,25 @@ $page_title = $page_title ?? 'Painel';
$user = $user ?? current_user() ?? ['name' => 'Visitante', 'role' => 'client'];
$app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
?><!doctype html>
<html lang="pt">
<html lang="pt-PT">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($page_title) ?> — <?= htmlspecialchars($app_name) ?></title>
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#eef0f4" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0a0a0a" media="(prefers-color-scheme: dark)">
<script>
// Tema antes do CSS, para não piscar o tema errado. Escolha guardada no
// navegador (claro / escuro / auto); auto segue o sistema operativo.
(function () {
var pref = 'auto';
try { pref = localStorage.getItem('bo-theme') || 'auto'; } catch (e) {}
var dark = pref === 'dark' || (pref === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.setAttribute('data-bs-theme', dark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme-pref', pref);
})();
</script>
<link href="<?= base_url('assets/vendor/bootstrap5/css/bootstrap.min.css') ?>" rel="stylesheet">
<link href="<?= base_url('assets/vendor/bootstrap-icons/bootstrap-icons.css') ?>" rel="stylesheet">
<link href="<?= base_url('assets/vendor/summernote/summernote-bs5.min.css') ?>" rel="stylesheet">
@@ -22,6 +36,7 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
--bo-dark-2: #151515;
--bo-surface: #eef0f4;
}
[data-bs-theme="dark"] { --bo-surface: #121416; }
body { background: var(--bo-surface); font-size: .925rem; }
.sidebar { position: fixed; top: 0; bottom: 0; left: 0; z-index: 100;
padding: 68px 0 0; width: 240px; background: var(--bo-dark); overflow-y: auto; }
@@ -38,7 +53,7 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
letter-spacing: 1.5px; padding: 1.25rem 1.25rem .35rem; }
main { margin-left: 240px; padding-top: 68px; }
.topbar { position: fixed; top: 0; left: 240px; right: 0; z-index: 99; height: 56px;
background: #fff; border-bottom: 1px solid #e2e6ec; padding: 0 1.5rem;
background: var(--bs-body-bg); border-bottom: 1px solid var(--bs-border-color); padding: 0 1.5rem;
display: flex; align-items: center; justify-content: space-between; }
.topbar .user-dropdown { display: flex; align-items: center; gap: .5rem; margin-left: auto; }
.badge-role-sysadmin { background: var(--bo-accent); color: var(--bo-dark); }
@@ -50,17 +65,19 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
.btn-primary { background: var(--bo-accent); border-color: var(--bo-accent); color: var(--bo-dark); }
.btn-primary:hover { background: #a8ba00; border-color: #a8ba00; color: var(--bo-dark); }
.card { border: none; box-shadow: 0 1px 3px rgba(0,0,0,.04); }
a { color: #0a0a0a; }
[data-bs-theme="dark"] .table-light { --bs-table-bg: var(--bs-tertiary-bg); --bs-table-color: var(--bs-body-color); }
.theme-toggle { min-width: 44px; min-height: 44px; }
a { color: var(--bs-emphasis-color); }
/* Summernote (WYSIWYG) — align with the backoffice look */
.note-editor.note-frame { border-color: #ced4da; border-radius: .375rem; }
.note-editor .note-editing-area .note-editable { font-size: .95rem; }
.note-editor.note-frame.note-focused { border-color: var(--bo-accent); box-shadow: 0 0 0 .2rem rgba(var(--bo-accent-rgb), .25); }
.note-toolbar { background: #f8f9fa; }
.note-toolbar { background: var(--bs-tertiary-bg); }
.note-btn.btn-primary { background: var(--bo-accent); border-color: var(--bo-accent); color: var(--bo-dark); }
/* ── Mobile: off-canvas sidebar + hamburger ──────────────────────────── */
.sidebar { transition: transform .25s ease; }
.menu-toggle { display: none; width: 40px; height: 40px; padding: 0; border: 1px solid #e2e6ec;
background: #fff; color: var(--bo-dark); border-radius: 8px; font-size: 1.35rem;
.menu-toggle { display: none; width: 44px; height: 44px; padding: 0; border: 1px solid var(--bs-border-color);
background: var(--bs-body-bg); color: var(--bs-body-color); border-radius: 8px; font-size: 1.35rem;
line-height: 1; align-items: center; justify-content: center; }
.sidebar-backdrop { display: none; position: fixed; inset: 0; z-index: 1090; background: rgba(0,0,0,.45); }
.sidebar-backdrop.show { display: block; }
@@ -228,6 +245,17 @@ $app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
</form>
<?php endif; ?>
<div class="user-dropdown">
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary theme-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false" aria-label="Tema: automático" id="themeToggle">
<i class="bi bi-circle-half" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="themeToggle">
<li><button type="button" class="dropdown-item" data-theme-set="light"><i class="bi bi-sun me-2" aria-hidden="true"></i>Claro</button></li>
<li><button type="button" class="dropdown-item" data-theme-set="dark"><i class="bi bi-moon-stars me-2" aria-hidden="true"></i>Escuro</button></li>
<li><button type="button" class="dropdown-item" data-theme-set="auto"><i class="bi bi-circle-half me-2" aria-hidden="true"></i>Automático</button></li>
</ul>
</div>
<span class="badge rounded-pill badge-role-<?= htmlspecialchars($user['role']) ?>">
<?= htmlspecialchars(role_label($user['role'])) ?>
</span>