- Pedido de aula experimental passa a ficar em trial_leads (por ginásio) ANTES do email: sem SMTP o visitante já não vê «erro» e o pedido não se perde. Gestão → Aulas experimentais: estados, notas, CSV (auditado), apagar para a lixeira; aviso «sem email» quando o aviso não saiu. - Aula experimental, inscrição e eventos usam o anti-spam do platform-tools (honeypot, carimbo assinado com tempo mínimo, prova de JS, limite por IP, pontuação de conteúdo) e o bloqueio de IP, em vez de honeypots locais. Recusas ficam em blocked_submissions; resposta igual à de sucesso. Falha aberto se o módulo rebentar. - Checkbox de consentimento da aula experimental passa a ser guardada. - Audit das escritas de conteúdo: criar/editar equipa, modalidades, eventos, mensalidades, pedidos de inscrição, galerias, horário (incl. arrastar no calendário), horas, contactos e ginásios, com antes/depois campo a campo (audit_row/audit_change). Exportação CSV de inscrições auditada. - .table-responsive em todas as tabelas do backoffice. - Migração: database/migration_20260926_trial_leads.sql (só cria tabelas). Testado numa cópia da BD de produção: 63/63 verificações HTTP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
236 lines
12 KiB
PHP
236 lines
12 KiB
PHP
<?php
|
||
// ── Event landing page ──────────────────────────────────────────────────────
|
||
// Branded, time-limited landing page reached at /evento/<slug> (see .htaccess).
|
||
// Visible to the public only while the event is live (db_events_active rule);
|
||
// logged-in staff may preview drafts/scheduled/expired events. Carries an
|
||
// optional lead-capture form that stores submissions in event_leads.
|
||
require_once __DIR__ . '/../config/db.php';
|
||
require_once __DIR__ . '/../config/functions.php';
|
||
|
||
// Resolve any SSO session up front (mirrors _open.php) so the is_staff() preview
|
||
// gate below sees a logged-in admin. _open.php's later call then no-ops.
|
||
$slug = preg_replace('/[^a-z0-9\-]/', '', strtolower((string) ($_GET['slug'] ?? '')));
|
||
$event = $slug !== '' ? db_events_get_by_slug($pdo, $slug) : null;
|
||
|
||
// Gate: live for everyone, otherwise only staff (preview).
|
||
$can_view = $event && (events_is_live($event) || is_staff());
|
||
|
||
// ── Lead form submission (PRG) ──────────────────────────────────────────────
|
||
$lead_errors = [];
|
||
if ($can_view && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
csrf_check();
|
||
$back = base_url('evento/' . rawurlencode($event['slug']));
|
||
|
||
// Anti-spam partilhado (config/functions_forms.php). Recusado = mesma
|
||
// resposta de sucesso; fica em blocked_submissions.
|
||
if (public_form_screen($pdo, 'evento', 'event_leads',
|
||
(string) ($_POST['lead_name'] ?? ''), (string) ($_POST['lead_message'] ?? '')) !== null) {
|
||
header('Location: ' . $back . '?ok=1#inscricao');
|
||
exit;
|
||
}
|
||
|
||
// The exact wording the visitor consents to — stored for GDPR evidence.
|
||
$consent_text = 'Autorizo o contacto e o tratamento dos meus dados para efeitos desta inscrição, nos termos da Política de Privacidade.';
|
||
$lead = [
|
||
'name' => trim((string) ($_POST['lead_name'] ?? '')),
|
||
'email' => trim((string) ($_POST['lead_email'] ?? '')),
|
||
'phone' => trim((string) ($_POST['lead_phone'] ?? '')),
|
||
'message' => trim((string) ($_POST['lead_message'] ?? '')),
|
||
'consent' => !empty($_POST['lead_consent']) ? 1 : 0,
|
||
'consent_text' => $consent_text,
|
||
// IP real atrás do proxy (config/env.php) — o primeiro valor do
|
||
// X-Forwarded-For é escrito pelo cliente.
|
||
'ip' => client_ip(),
|
||
'user_agent' => (string) ($_SERVER['HTTP_USER_AGENT'] ?? ''),
|
||
];
|
||
if ($lead['name'] === '') $lead_errors[] = 'Indica o teu nome.';
|
||
if ($lead['email'] === '' && $lead['phone'] === '') $lead_errors[] = 'Indica um email ou um telefone para te podermos contactar.';
|
||
if ($lead['email'] !== '' && !filter_var($lead['email'], FILTER_VALIDATE_EMAIL)) $lead_errors[] = 'O email indicado não é válido.';
|
||
if (!$lead['consent']) $lead_errors[] = 'É necessário aceitar o tratamento dos dados para enviar a inscrição.';
|
||
|
||
if (!$lead_errors) {
|
||
db_event_lead_create($pdo, (int) $event['e_id'], $lead);
|
||
header('Location: ' . $back . '?ok=1#inscricao');
|
||
exit;
|
||
}
|
||
// fall through and re-render with $lead_errors + the submitted $lead values
|
||
$lead_old = $lead;
|
||
}
|
||
|
||
// ── Not found / not visible ─────────────────────────────────────────────────
|
||
if (!$can_view) {
|
||
http_response_code(404);
|
||
$current_uri_2 = '';
|
||
require __DIR__ . '/_open.php';
|
||
?>
|
||
<section class="event-notfound" style="padding:160px 0 90px;text-align:center;">
|
||
<div class="container">
|
||
<h1 style="font-size:2.4rem;">Evento não encontrado</h1>
|
||
<p class="text-muted">Este evento pode já ter terminado ou o link estar incorreto.</p>
|
||
<a href="<?= base_url('/') ?>" class="site-btn mt-3">Voltar ao início</a>
|
||
</div>
|
||
</section>
|
||
<?php
|
||
echo view('footer');
|
||
return;
|
||
}
|
||
|
||
// ── Visible: render the landing page ────────────────────────────────────────
|
||
$is_preview = !events_is_live($event);
|
||
$hero_url = $event['hero_image'] !== '' ? base_url('uploads/events/' . rawurlencode($event['hero_image'])) : '';
|
||
$accent = $event['cor'] !== '' ? $event['cor'] : '#C2D500';
|
||
|
||
// Human date line for the event window.
|
||
$date_line = '';
|
||
if (!empty($event['event_starts_at'])) {
|
||
$date_line = format_date_pt((string) $event['event_starts_at']);
|
||
if (!empty($event['event_ends_at'])
|
||
&& substr((string) $event['event_ends_at'], 0, 10) !== substr((string) $event['event_starts_at'], 0, 10)) {
|
||
$date_line .= ' – ' . format_date_pt((string) $event['event_ends_at']);
|
||
}
|
||
}
|
||
|
||
// CTA target: explicit URL, else the on-page form (when shown), else contacts.
|
||
$cta_label = trim((string) $event['cta_label']);
|
||
$cta_href = trim((string) $event['cta_url']);
|
||
if ($cta_href === '') {
|
||
$cta_href = ((int) $event['show_lead_form'] === 1) ? '#inscricao' : base_url('home/contactos');
|
||
}
|
||
|
||
// ── SEO ─────────────────────────────────────────────────────────────────────
|
||
// include.php / seo.php honour these globals when set before _open.php runs.
|
||
$event_url = base_url('evento/' . rawurlencode($event['slug']));
|
||
$seo_desc = $event['summary'] !== ''
|
||
? $event['summary']
|
||
: trim(mb_substr(strip_tags((string) ($event['subtitle'] ?: $event['description'])), 0, 200));
|
||
|
||
$current_uri_2 = 'evento';
|
||
$GLOBALS['_page_meta_override'] = [
|
||
($event['title'] !== '' ? $event['title'] : 'Evento') . ' | FitMe 3070',
|
||
$seo_desc,
|
||
];
|
||
$GLOBALS['_canonical_override'] = $event_url;
|
||
$GLOBALS['_og_type_override'] = 'article';
|
||
if ($hero_url !== '') $GLOBALS['_og_image_override'] = $hero_url;
|
||
|
||
// Event structured data (schema.org/Event) for rich link previews / search.
|
||
$info = site_info($pdo);
|
||
$ld_event = [
|
||
'@context' => 'https://schema.org',
|
||
'@type' => 'Event',
|
||
'name' => $event['title'],
|
||
'description' => $seo_desc !== '' ? $seo_desc : $event['title'],
|
||
'url' => $event_url,
|
||
'eventStatus' => 'https://schema.org/EventScheduled',
|
||
'eventAttendanceMode' => 'https://schema.org/OfflineEventAttendanceMode',
|
||
];
|
||
if ($hero_url !== '') $ld_event['image'] = [$hero_url];
|
||
if (!empty($event['event_starts_at'])) $ld_event['startDate'] = str_replace(' ', 'T', (string) $event['event_starts_at']);
|
||
if (!empty($event['event_ends_at'])) $ld_event['endDate'] = str_replace(' ', 'T', (string) $event['event_ends_at']);
|
||
$ld_event['location'] = $event['location'] !== ''
|
||
? ['@type' => 'Place', 'name' => $event['location']]
|
||
: ['@type' => 'Place',
|
||
'name' => ($info['name'] ?? '') !== '' ? $info['name'] : 'FitMe 3070',
|
||
'address' => $info['adress'] ?? ''];
|
||
$ld_event['organizer'] = [
|
||
'@type' => 'Organization',
|
||
'name' => ($info['name'] ?? '') !== '' ? $info['name'] : 'FitMe 3070',
|
||
'url' => base_url(),
|
||
];
|
||
$GLOBALS['_jsonld_extra'] = $ld_event;
|
||
|
||
require __DIR__ . '/_open.php';
|
||
?>
|
||
<?php if ($is_preview): ?>
|
||
<div class="event-preview-bar">Pré-visualização — este evento não está visível ao público (<?= htmlspecialchars(events_status($event)) ?>).</div>
|
||
<?php endif; ?>
|
||
|
||
<section class="event-hero set-bg<?= $hero_url ? '' : ' event-hero--plain' ?>"
|
||
<?= $hero_url ? 'style="background-image:url(\'' . htmlspecialchars($hero_url, ENT_QUOTES) . '\')"' : '' ?>>
|
||
<div class="event-hero-overlay"></div>
|
||
<div class="container">
|
||
<div class="event-hero-inner">
|
||
<span class="event-hero-eyebrow" style="color:<?= htmlspecialchars($accent, ENT_QUOTES) ?>">Evento</span>
|
||
<h1><?= htmlspecialchars($event['title']) ?></h1>
|
||
<?php if ($event['subtitle'] !== ''): ?><p class="event-hero-sub"><?= htmlspecialchars($event['subtitle']) ?></p><?php endif; ?>
|
||
<ul class="event-hero-meta">
|
||
<?php if ($date_line !== ''): ?><li><i class="fas fa-calendar-alt"></i> <?= htmlspecialchars($date_line) ?></li><?php endif; ?>
|
||
<?php if ($event['location'] !== ''): ?><li><i class="fas fa-map-marker-alt"></i> <?= htmlspecialchars($event['location']) ?></li><?php endif; ?>
|
||
</ul>
|
||
<?php if ($cta_label !== ''): ?>
|
||
<a href="<?= htmlspecialchars($cta_href, ENT_QUOTES) ?>" class="site-btn event-hero-cta"
|
||
style="background:<?= htmlspecialchars($accent, ENT_QUOTES) ?>"
|
||
<?= ($cta_href[0] ?? '') === '#' ? 'data-scroll' : '' ?>><?= htmlspecialchars($cta_label) ?></a>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<?php if (trim((string) $event['description']) !== ''): ?>
|
||
<section class="event-body">
|
||
<div class="container">
|
||
<div class="row justify-content-md-center">
|
||
<div class="col-lg-8">
|
||
<?php /* description is admin-authored HTML — sanitised allowlist HTML */ ?>
|
||
<?= clean_richtext($event['description']) ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<?php endif; ?>
|
||
|
||
<?php if ((int) $event['show_lead_form'] === 1): ?>
|
||
<section class="event-leadform" id="inscricao">
|
||
<div class="container">
|
||
<div class="row justify-content-md-center">
|
||
<div class="col-lg-7">
|
||
<div class="section-title text-center deco">
|
||
<span class="section-eyebrow">Participa</span>
|
||
<h2>Inscreve-te</h2>
|
||
<p class="section-subtitle">Deixa os teus dados e entramos em contacto.</p>
|
||
</div>
|
||
|
||
<?php if (isset($_GET['ok'])): ?>
|
||
<div class="event-alert event-alert--ok">Inscrição enviada com sucesso. Obrigado!</div>
|
||
<?php endif; ?>
|
||
<?php foreach ($lead_errors as $e): ?>
|
||
<div class="event-alert event-alert--err"><?= htmlspecialchars($e) ?></div>
|
||
<?php endforeach; ?>
|
||
|
||
<form method="POST" action="<?= base_url('evento/' . rawurlencode($event['slug'])) ?>#inscricao" class="event-form">
|
||
<?= csrf_field() ?>
|
||
<?= public_form_trap('evento') /* honeypot + carimbo + prova de JS */ ?>
|
||
<div class="event-form-grid">
|
||
<div class="event-field">
|
||
<label>Nome *</label>
|
||
<input type="text" name="lead_name" required maxlength="150" value="<?= htmlspecialchars($lead_old['name'] ?? '') ?>">
|
||
</div>
|
||
<div class="event-field">
|
||
<label>Telefone</label>
|
||
<input type="tel" name="lead_phone" maxlength="40" value="<?= htmlspecialchars($lead_old['phone'] ?? '') ?>">
|
||
</div>
|
||
<div class="event-field event-field--full">
|
||
<label>Email</label>
|
||
<input type="email" name="lead_email" maxlength="190" value="<?= htmlspecialchars($lead_old['email'] ?? '') ?>">
|
||
</div>
|
||
<div class="event-field event-field--full">
|
||
<label>Mensagem</label>
|
||
<textarea name="lead_message" rows="3" maxlength="1000"><?= htmlspecialchars($lead_old['message'] ?? '') ?></textarea>
|
||
</div>
|
||
</div>
|
||
<label class="event-consent">
|
||
<input type="checkbox" name="lead_consent" value="1" required <?= !empty($lead_old['consent']) ? 'checked' : '' ?>>
|
||
<span>Autorizo o contacto e o tratamento dos meus dados para esta inscrição, nos termos da <a href="<?= base_url('home/privacidade') ?>" target="_blank" rel="noopener">Política de Privacidade</a>.</span>
|
||
</label>
|
||
<div class="text-center mt-3">
|
||
<button type="submit" class="site-btn" style="background:<?= htmlspecialchars($accent, ENT_QUOTES) ?>">Enviar inscrição</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<?php endif; ?>
|
||
<?php
|
||
echo view('footer');
|