feat: vários ginásios (/3070/ e /<código postal>/), CRUD de ginásios, modalidades após a galeria
Estrutura multi-ginásio: as mesmas secções do site, conteúdo próprio por ginásio, e cada um numa pasta da URL com o código postal da zona. - migration_20260926_gyms: tabela gyms (pasta, nome, localidade, publicado, principal, secções visíveis, título/imagem do header, SEO, galeria, Place ID, página CrossFit) com o FitMe 3070 como ginásio 1 em /3070/; gym_id em team, modalidades (o horário segue a modalidade), work_hours, memberships, membership_leads e website_offer (tudo o existente → ginásio 1); events com gym_id NULL = todos; fitme3070_info passa a uma linha por ginásio (id = gym). - Site: /<pasta>/, /<pasta>/crossfit, /<pasta>/agendar|inscricao. Ginásio por publicar → 404 para o público, pré-visualização (com faixa) para a equipa. / e /home → 301 para o ginásio principal (a manter a query); com mais de um ginásio publicado, a raiz passa a uma página para escolher. Secções desligadas não aparecem nem no menu; menu, canonical, JSON-LD, sitemap, formulários, e-mails e reviews (Place ID e cache) são do ginásio. - Backoffice: Gestão → Ginásios (criar/editar/apagar, secções, e «começar com o conteúdo de» outro ginásio — cópias independentes) e o seletor «A editar» no topo; equipa, modalidades, horários, horário de funcionamento, mensalidades, pedidos, contactos e reviews trabalham sobre o ginásio escolhido, e um registo de outro ginásio não abre. - Modalidades passam para logo a seguir à galeria das instalações. - Galeria sem cantos arredondados (raio da marca). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2f8f68c61a
commit
5e300fc341
33 files changed
+1146
-197
No files matched your search
+16
-3
@@ -5,14 +5,27 @@
|
||||
require_once __DIR__ . '/../config/db.php';
|
||||
require_once __DIR__ . '/../config/functions.php';
|
||||
|
||||
$info = site_info($pdo);
|
||||
$navbar = site_navbar();
|
||||
// The gym this page belongs to: ?gym=<slug> from the URL folder (/3070/…), or
|
||||
// the default gym for pages outside a gym folder (privacy page, events).
|
||||
// Unknown slugs — and unpublished gyms for anyone not logged in to the
|
||||
// backoffice — are a plain 404.
|
||||
$gym = site_gym($pdo);
|
||||
if (!$gym) { require __DIR__ . '/404.php'; exit; }
|
||||
$GLOBALS['_gym'] = $gym;
|
||||
|
||||
$info = site_info($pdo, (int) $gym['id']);
|
||||
$navbar = site_navbar($gym);
|
||||
$current_uri_2 = $current_uri_2 ?? '';
|
||||
$GLOBALS['_view_shared'] = compact('info', 'navbar', 'current_uri_2');
|
||||
$GLOBALS['_view_shared'] = compact('info', 'navbar', 'current_uri_2', 'gym');
|
||||
|
||||
echo view('include'); // renders view('seo') inside <head>
|
||||
echo view('navbar');
|
||||
echo view('messenger_chat');
|
||||
// Staff previewing a gym that isn't public yet.
|
||||
if (empty($gym['is_published'])) {
|
||||
echo '<div class="event-preview-bar">Pré-visualização — <strong>' . htmlspecialchars($gym['name'])
|
||||
. '</strong> ainda não está publicado. Só quem tem sessão iniciada no backoffice vê esta página.</div>' . "\n";
|
||||
}
|
||||
// Page content landmark — closed at the top of site/includes/footer.php, so
|
||||
// every public page gets one <main> (and the skip link a real target).
|
||||
echo '<main id="conteudo">' . "\n";
|
||||
+11
-6
@@ -8,16 +8,21 @@
|
||||
require_once __DIR__ . '/../config/db.php';
|
||||
require_once __DIR__ . '/../config/functions.php';
|
||||
|
||||
// Only POST is meaningful here; anything else just goes home.
|
||||
// The gym comes from the URL folder (/<slug>/agendar); the old /home/agendar
|
||||
// route has no ?gym and falls back to the default gym.
|
||||
$gym = site_gym($pdo) ?? db_gyms_default($pdo) ?? gym_fallback();
|
||||
$GLOBALS['_gym'] = $gym;
|
||||
|
||||
// Only POST is meaningful here; anything else just goes to the gym's page.
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
header('Location: ' . base_url('home'));
|
||||
header('Location: ' . gym_url($gym));
|
||||
exit;
|
||||
}
|
||||
|
||||
csrf_check(); // 403s and exits on a bad/missing token
|
||||
|
||||
function agendar_redirect(string $status): void {
|
||||
header('Location: ' . base_url('home?agendar=' . $status . '#contactos'));
|
||||
header('Location: ' . gym_url(current_gym(), '?agendar=' . $status . '#contactos'));
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -39,7 +44,7 @@ if ($data !== '') {
|
||||
$data_fmt = ($d && $d->format('Y-m-d') === $data) ? $d->format('d/m/Y') : '';
|
||||
}
|
||||
|
||||
$info = site_info($pdo);
|
||||
$info = site_info($pdo, (int) $gym['id']);
|
||||
$to = $info['email'] !== '' ? $info['email'] : EMAIL_FROM;
|
||||
|
||||
$e = static fn($v) => htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
|
||||
@@ -58,7 +63,7 @@ foreach ($rows as $label => $value) {
|
||||
. ':</td><td>' . nl2br($e($value)) . '</td></tr>';
|
||||
}
|
||||
$body .= '</table><p style="font-family:Arial,sans-serif;font-size:13px;color:#666;margin-top:18px;">'
|
||||
. 'Pedido enviado através do formulário do site FitMe 3070. Responde diretamente a '
|
||||
. 'Pedido enviado através do formulário do site ' . $e($gym['name']) . '. Responde diretamente a '
|
||||
. $e($email) . ' ou liga para ' . $e($telefone) . '.</p>';
|
||||
|
||||
$alt = "Novo pedido de aula experimental\n\n";
|
||||
@@ -69,7 +74,7 @@ foreach ($rows as $label => $value) {
|
||||
|
||||
$id = enqueue_email([
|
||||
'to_address' => $to,
|
||||
'to_name' => $info['name'] !== '' ? $info['name'] : 'FitMe 3070',
|
||||
'to_name' => $info['name'] !== '' ? $info['name'] : $gym['name'],
|
||||
'subject' => 'Pedido de aula experimental — ' . $nome,
|
||||
'body' => $body,
|
||||
'alt_body' => $alt,
|
||||
|
||||
@@ -4149,7 +4149,7 @@ h3.contact-title {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--fitme-radius); /* brand: square-ish corners */
|
||||
position: relative;
|
||||
background: var(--fitme-surface-2);
|
||||
cursor: zoom-in;
|
||||
@@ -4229,7 +4229,7 @@ h3.contact-title {
|
||||
max-width: 100%;
|
||||
max-height: calc(100vh - 110px);
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--fitme-radius);
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, .5);
|
||||
animation: gal-in .35s ease;
|
||||
}
|
||||
|
||||
+14
-2
@@ -1,7 +1,19 @@
|
||||
<?php
|
||||
// Was Home::crossfit.
|
||||
$current_uri_2 = 'crossfit';
|
||||
|
||||
// Old /home/crossfit links → the default gym's CrossFit page.
|
||||
if (($_GET['gym'] ?? '') === '') {
|
||||
require_once __DIR__ . '/../config/db.php';
|
||||
require_once __DIR__ . '/../config/functions.php';
|
||||
$to = db_gyms_default($pdo) ?? gym_fallback();
|
||||
header('Location: ' . gym_url($to, 'crossfit'), true, 301);
|
||||
exit;
|
||||
}
|
||||
|
||||
require __DIR__ . '/_open.php';
|
||||
// Only gyms with a CrossFit box have this page.
|
||||
if (empty($gym['has_crossfit'])) { ob_get_level() && ob_end_clean(); require __DIR__ . '/404.php'; exit; }
|
||||
|
||||
$data = [
|
||||
'title' => 'CrossFit',
|
||||
@@ -13,13 +25,13 @@ $data = [
|
||||
'header_video_video' => 'site/assets/img/crossfit.mp4',
|
||||
'header_video_img' => 'site/assets/img/crossfit.jpg',
|
||||
'rel_footer_h2' => 'Precisas de um<span>Personal Trainer?</span><br> Entra em contacto connosco.',
|
||||
'ad_footer_link' => 'home/contactos?assunto=pt_cf',
|
||||
'ad_footer_link' => rawurlencode($gym['slug']) . '/?assunto=pt_cf#contactos',
|
||||
'ad_footer_text' => 'Tens objetivos específicos na área do CrossFit? Temos a solução para ti. Atinge e melhora os teus resultados com os nossos profissionais especializados na área. Todas as viagens são melhores quando vividas com a companhia certa. ',
|
||||
'ad_footer_h2' => 'Precisas de um <span>Personal Trainer</span> especializado em CrossFit?',
|
||||
'ad_footer_bg' => '',
|
||||
'ad_footer_text_btn' => 'Entra em contacto connosco',
|
||||
];
|
||||
$data['data'] = site_horario($pdo, 'crossfit');
|
||||
$data['data'] = site_horario($pdo, 'crossfit', (int) $gym['id']);
|
||||
|
||||
echo view('header_video', $data);
|
||||
echo view('crossfit', $data);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
// Root page when more than one gym is published (site/index.php includes this
|
||||
// instead of redirecting): pick a gym. Self-contained — no gym is "current"
|
||||
// here, so it doesn't go through _open.php. Expects $pdo and $published.
|
||||
$gyms = $published ?? db_gyms_published($pdo);
|
||||
$brand = 'FitMe';
|
||||
?><!doctype html>
|
||||
<html lang="pt">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= htmlspecialchars($brand) ?> — Escolhe o teu ginásio</title>
|
||||
<meta name="description" content="<?= htmlspecialchars($brand) ?>: <?= htmlspecialchars(implode(', ', array_map(fn($g) => $g['name'] . ($g['locality'] !== '' ? ' (' . $g['locality'] . ')' : ''), $gyms))) ?>.">
|
||||
<link rel="canonical" href="<?= htmlspecialchars(base_url()) ?>">
|
||||
<?php if (seo_noindex()): ?><meta name="robots" content="noindex, nofollow"><?php endif; ?>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:400,700,900&display=swap" rel="stylesheet">
|
||||
<link rel="icon" type="image/svg+xml" href="<?= base_url('site/assets/img/logos/logo_black.svg') ?>">
|
||||
<style>
|
||||
:root { --accent:#C2D500; --dark:#0B0B0D; --dark2:#16181A; --ash:#B8B9BE; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin:0; min-height:100svh; font-family:Roboto,system-ui,sans-serif; color:#fff;
|
||||
background:#141517 url('<?= base_url('site/assets/img/home.jpg') ?>') center/cover fixed; }
|
||||
.wrap { min-height:100svh; background:rgba(11,11,13,.82); display:flex; flex-direction:column;
|
||||
align-items:center; justify-content:center; padding:48px 16px; gap:36px; }
|
||||
h1 { margin:0; font-size:clamp(1.8rem,4vw,2.6rem); font-weight:900; text-transform:uppercase; text-align:center; }
|
||||
h1 span { color:var(--accent); }
|
||||
.gyms { display:flex; flex-wrap:wrap; gap:18px; justify-content:center; width:100%; max-width:900px; }
|
||||
a.gym { flex:1 1 260px; max-width:320px; display:block; padding:28px 24px; border-radius:8px; text-decoration:none;
|
||||
color:#fff; background:var(--dark2); border:2px solid rgba(255,255,255,.08);
|
||||
transition:border-color .25s, transform .25s; }
|
||||
a.gym:hover, a.gym:focus-visible { border-color:var(--accent); transform:translateY(-4px); outline:none; }
|
||||
.gym strong { display:block; font-size:1.4rem; font-weight:900; text-transform:uppercase; }
|
||||
.gym span { color:var(--ash); }
|
||||
.gym em { display:inline-block; margin-top:14px; font-style:normal; font-weight:700; color:var(--accent); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrap">
|
||||
<img src="<?= base_url('site/assets/img/logos/logo_black.svg') ?>" alt="<?= htmlspecialchars($brand) ?>" style="width:170px;filter:invert(1)">
|
||||
<h1>Escolhe o teu <span>ginásio</span></h1>
|
||||
<div class="gyms">
|
||||
<?php foreach ($gyms as $g): ?>
|
||||
<a class="gym" href="<?= htmlspecialchars(gym_url($g)) ?>">
|
||||
<strong><?= htmlspecialchars($g['name']) ?></strong>
|
||||
<?php if ($g['locality'] !== ''): ?><span><?= htmlspecialchars($g['locality']) ?></span><?php endif; ?>
|
||||
<br><em>Entrar →</em>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -21,7 +21,7 @@
|
||||
<?php endif; ?>
|
||||
<ul class="footer-menu">
|
||||
<?php foreach($navbar as $key => $n): ?>
|
||||
<li><a href="<?= base_url($n) ?>" <?= (isset($n[0]) && $n[0] === '#') ? 'class="smooth-scroll"' : '' ?>><?= $key ?></a></li>
|
||||
<li><a href="<?= htmlspecialchars($n) ?>" <?= str_contains($n, '#') ? 'class="smooth-scroll"' : '' ?>><?= $key ?></a></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<p class="small footer-legal">Este estabelecimento possui livro de reclamações online em <a href="https://www.livroreclamacoes.pt/Pedido/Reclamacao" target="_blank" rel="noopener">www.livroreclamacoes.pt</a></p>
|
||||
|
||||
@@ -10,10 +10,25 @@ $page_meta = [
|
||||
'privacidade' => ['Política de Privacidade | FitMe 3070',
|
||||
'Política de privacidade do FitMe 3070 — como recolhemos, usamos e protegemos os teus dados pessoais.'],
|
||||
];
|
||||
// The gym's homepage uses the gym's own SEO title/description when set
|
||||
// (Gestão → Ginásios); a new gym without them gets a generic one from its name
|
||||
// and locality rather than the 3070 texts above.
|
||||
$gym_meta = current_gym();
|
||||
if ($page === '' && ($gym_meta['seo_title'] ?? '') !== '') {
|
||||
$page_meta[''] = [$gym_meta['seo_title'], ($gym_meta['seo_description'] ?? '') !== '' ? $gym_meta['seo_description'] : $page_meta[''][1]];
|
||||
} elseif ($page === '' && (int) $gym_meta['id'] !== 1) {
|
||||
$loc = ($gym_meta['locality'] ?? '') !== '' ? ' em ' . $gym_meta['locality'] : '';
|
||||
$page_meta[''] = [$gym_meta['name'] . ' — Ginásio' . $loc,
|
||||
($gym_meta['seo_description'] ?? '') !== '' ? $gym_meta['seo_description'] : 'Ginásio ' . $gym_meta['name'] . $loc . ': treino, aulas de grupo e acompanhamento.'];
|
||||
}
|
||||
// A dynamic page (e.g. an event landing page) can set its own title/description
|
||||
// and canonical via these globals before requiring _open.php.
|
||||
[$page_title, $page_desc] = $GLOBALS['_page_meta_override'] ?? $page_meta[$page] ?? $page_meta[''];
|
||||
$canonical = $GLOBALS['_canonical_override'] ?? base_url($page !== '' ? 'home/' . $page : '');
|
||||
$canonical = $GLOBALS['_canonical_override'] ?? match ($page) {
|
||||
'' => gym_url($gym_meta),
|
||||
'crossfit' => gym_url($gym_meta, 'crossfit'),
|
||||
default => base_url('home/' . $page),
|
||||
};
|
||||
?><!doctype html>
|
||||
<html lang="pt">
|
||||
<head>
|
||||
@@ -34,7 +49,7 @@ $canonical = $GLOBALS['_canonical_override'] ?? base_url($page !== '' ? 'home/'
|
||||
<link rel="dns-prefetch" href="https://forms.zohopublic.eu">
|
||||
<?php if (($current_uri_2 ?? '') === ''): ?>
|
||||
<!-- Homepage hero is the LCP image — start fetching it before the CSS/JS -->
|
||||
<link rel="preload" as="image" href="<?= base_url('site/assets/img/home.jpg') ?>">
|
||||
<link rel="preload" as="image" href="<?= base_url(($gym_meta['hero_image'] ?? '') !== '' ? $gym_meta['hero_image'] : 'site/assets/img/home.jpg') ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Fonts -->
|
||||
|
||||
@@ -9,18 +9,20 @@ else
|
||||
?>
|
||||
<header class="header-section lazy">
|
||||
<nav class="navbar navbar-expand-lg navbar-light" style="padding-left:1rem; padding-top: 0; padding-bottom: 0;padding-right: 0; ">
|
||||
<a class="navbar-brand" href="<?= base_url('home') ?>">
|
||||
<a class="navbar-brand" href="<?= htmlspecialchars(gym_url(current_gym())) ?>">
|
||||
<img loading="eager" alt="Logo FitMe3070" src="<?= $logo ?>" style="width: 150px;">
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mx-auto ">
|
||||
<?php foreach($navbar as $key => $n):
|
||||
// $n is an absolute URL on the gym's own page (site_navbar()).
|
||||
$is_crossfit = ($key === 'CrossFit');
|
||||
$is_anchor = str_contains($n, '#');
|
||||
$is_active = false;
|
||||
if ($is_crossfit) {
|
||||
$is_active = ($current_uri_2 === 'crossfit');
|
||||
} elseif ($n === '#home') {
|
||||
} elseif (str_ends_with($n, '#home')) {
|
||||
// Only the Home link is active by default on the homepage
|
||||
$is_active = ($current_uri_2 === '');
|
||||
}
|
||||
@@ -28,16 +30,15 @@ else
|
||||
?>
|
||||
<li class="nav-item <?= $is_active ? 'active' : '' ?> main-menu-li" >
|
||||
<?php if ($is_crossfit): ?>
|
||||
<a class="nav-link crossfit-nav-link" href="<?= base_url($n) ?>"<?= $is_active ? ' aria-current="page"' : '' ?>>
|
||||
<a class="nav-link crossfit-nav-link" href="<?= htmlspecialchars($n) ?>"<?= $is_active ? ' aria-current="page"' : '' ?>>
|
||||
<?= $key ?>
|
||||
</a>
|
||||
<?php elseif (isset($n[0]) && $n[0] === '#'): ?>
|
||||
<?php $href = ($current_uri_2 === 'crossfit') ? base_url('home' . $n) : base_url($n); ?>
|
||||
<a class="nav-link smooth-scroll" href="<?= $href ?>"<?= $is_active ? ' aria-current="page"' : '' ?>>
|
||||
<?php elseif ($is_anchor): ?>
|
||||
<a class="nav-link smooth-scroll" href="<?= htmlspecialchars($n) ?>"<?= $is_active ? ' aria-current="page"' : '' ?>>
|
||||
<?= $key ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<a class="nav-link" href="<?= base_url($n) ?>">
|
||||
<a class="nav-link" href="<?= htmlspecialchars($n) ?>">
|
||||
<?= $key ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<?php
|
||||
// JSON-LD structured data for the gym (rendered inside <head> by include.php).
|
||||
// Built from fitme3070_info + work_hours so admin edits stay in sync.
|
||||
$ld_gym = current_gym();
|
||||
$ld = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'ExerciseGym',
|
||||
'name' => ($info['name'] ?? '') !== '' ? $info['name'] : 'FitMe 3070',
|
||||
'url' => base_url(),
|
||||
'image' => base_url('site/assets/img/home.jpg'),
|
||||
'name' => $ld_gym['name'] !== '' ? $ld_gym['name'] : (($info['name'] ?? '') !== '' ? $info['name'] : 'FitMe 3070'),
|
||||
'url' => gym_url($ld_gym),
|
||||
'image' => base_url(($ld_gym['hero_image'] ?? '') !== '' ? $ld_gym['hero_image'] : 'site/assets/img/home.jpg'),
|
||||
'logo' => base_url('site/assets/img/logos/logo_black.svg'),
|
||||
];
|
||||
if (!empty($info['phone'])) $ld['telephone'] = site_tel($info['phone']);
|
||||
@@ -15,7 +16,7 @@ if (!empty($info['adress'])) {
|
||||
$ld['address'] = [
|
||||
'@type' => 'PostalAddress',
|
||||
'streetAddress' => $info['adress'],
|
||||
'addressLocality' => 'Mira',
|
||||
'addressLocality' => ($ld_gym['locality'] ?? '') !== '' ? $ld_gym['locality'] : 'Mira',
|
||||
'addressCountry' => 'PT',
|
||||
];
|
||||
}
|
||||
|
||||
+94
-68
@@ -2,17 +2,37 @@
|
||||
// One-page site: Home + Equipa + Modalidades + Horário + Contactos.
|
||||
// CrossFit is a separate page (site/crossfit.php).
|
||||
$current_uri_2 = '';
|
||||
require __DIR__ . '/_open.php';
|
||||
|
||||
// No gym in the URL (/, /home, old links): send people to a gym page. With a
|
||||
// single published gym that is a 301 to it (query string kept, e.g.
|
||||
// ?agendar=ok); with more, the gym chooser.
|
||||
if (($_GET['gym'] ?? '') === '') {
|
||||
require_once __DIR__ . '/../config/db.php';
|
||||
require_once __DIR__ . '/../config/functions.php';
|
||||
$published = db_gyms_published($pdo);
|
||||
if (count($published) > 1) { require __DIR__ . '/gym_chooser.php'; exit; }
|
||||
$to = $published[0] ?? db_gyms_default($pdo) ?? gym_fallback();
|
||||
$qs = $_SERVER['QUERY_STRING'] ?? '';
|
||||
header('Location: ' . gym_url($to) . ($qs !== '' ? '?' . $qs : ''), true, 301);
|
||||
exit;
|
||||
}
|
||||
|
||||
require __DIR__ . '/_open.php'; // resolves $gym (404 if unknown / not visible)
|
||||
|
||||
// ── Load all data ───────────────────────────────────────────────────────────────
|
||||
$offer = site_offer($pdo);
|
||||
$team = site_team($pdo);
|
||||
$modalidades = site_modalidades($pdo);
|
||||
$horario = site_horario($pdo);
|
||||
$whours = site_work_hours($pdo);
|
||||
$events = site_active_events($pdo); // time-limited events, banner below the hero
|
||||
$memberships = site_memberships($pdo);
|
||||
$reviews = site_reviews(); // null when not configured / nothing qualifies
|
||||
// Everything is this gym's content; a section the gym switched off
|
||||
// (Gestão → Ginásios) loads as empty, so its block below simply isn't printed.
|
||||
$gid = (int) $gym['id'];
|
||||
$on = fn(string $key) => gym_section_on($gym, $key);
|
||||
$offer = $on('oferta') ? site_offer($pdo, $gid) : [];
|
||||
$team = $on('equipa') ? site_team($pdo, $gid) : [];
|
||||
$modalidades = $on('modalidades') ? site_modalidades($pdo, $gid) : [];
|
||||
$horario = $on('horario') ? site_horario($pdo, '', $gid) : [];
|
||||
$whours = $on('workhours') ? site_work_hours($pdo, $gid) : [];
|
||||
$events = site_active_events($pdo, $gid); // time-limited events, banner below the hero
|
||||
$memberships = $on('mensalidades') ? site_memberships($pdo, $gid) : [];
|
||||
$reviews = $on('reviews') ? site_reviews($gym) : null; // null when not configured / nothing qualifies
|
||||
$hero_img = $gym['hero_image'] !== '' ? $gym['hero_image'] : 'site/assets/img/home.jpg';
|
||||
|
||||
// ── Hero section ────────────────────────────────────────────────────────────────
|
||||
?>
|
||||
@@ -27,7 +47,7 @@ $reviews = site_reviews(); // null when not configured / nothin
|
||||
|
||||
<!-- Hero background is inline (no data-setbg) so it paints without waiting for JS;
|
||||
the set-bg class is kept for its cover/center styling. -->
|
||||
<section id="home" class="hero-section set-bg fade-in" style="background-image:url('<?= base_url('site/assets/img/home.jpg') ?>')">
|
||||
<section id="home" class="hero-section set-bg fade-in" style="background-image:url('<?= htmlspecialchars(base_url($hero_img), ENT_QUOTES) ?>')">
|
||||
<div class="container">
|
||||
<div class="hero-text">
|
||||
<?php /* Eventos a decorrer — destacados no topo do header. Cada um liga
|
||||
@@ -50,7 +70,7 @@ $reviews = site_reviews(); // null when not configured / nothin
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<h1>Ginásio, Box, Aulas<br><span>& Acompanhamento</span></h1>
|
||||
<h1><?= htmlspecialchars($gym['hero_title'] !== '' ? $gym['hero_title'] : $gym['name']) ?><?php if ($gym['hero_subtitle'] !== ''): ?><br><span><?= htmlspecialchars($gym['hero_subtitle']) ?></span><?php endif; ?></h1>
|
||||
<div class="hero-cta-row">
|
||||
<a href="?assunto=insc#contactos" class="site-btn">Inscreve-te Já!</a>
|
||||
<button type="button" class="site-btn sb-ghost" data-toggle="modal" data-target="#trialModal">Marca uma Aula Experimental</button>
|
||||
@@ -92,8 +112,8 @@ if ($offer):
|
||||
|
||||
<?php
|
||||
// ── Galeria (Instalações) ───────────────────────────────────────────────────────
|
||||
$gallery_path = 'uploads/gallery/1/';
|
||||
$gallery_files = glob($gallery_path . '*.jpg');
|
||||
$gallery_path = !empty($gym['gallery_id']) ? 'uploads/gallery/' . (int) $gym['gallery_id'] . '/' : '';
|
||||
$gallery_files = ($on('galeria') && $gallery_path !== '') ? glob($gallery_path . '*.jpg') : [];
|
||||
if ($gallery_files && count($gallery_files) > 0):
|
||||
$gallery_h2 = 'Conhece as nossas instalações';
|
||||
?>
|
||||
@@ -102,6 +122,59 @@ $gallery_h2 = 'Conhece as nossas instalações';
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
// ── Modalidades ─────────────────────────────────────────────────────────────────
|
||||
if ($modalidades):
|
||||
?>
|
||||
<section id="modalidades" class="service-section" aria-labelledby="modalidades-title">
|
||||
<div class="container">
|
||||
<div class="row justify-content-md-center mb-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="section-title text-center deco">
|
||||
<span class="section-eyebrow">Treinos</span>
|
||||
<h2 id="modalidades-title">Modalidades</h2>
|
||||
<p class="section-subtitle">Encontra a modalidade certa para ti.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php /* Full-width strip of modalidade cards. It is a real horizontal
|
||||
scroller (touch, trackpad/wheel, mouse drag, arrows) that also
|
||||
glides on its own until someone interacts (main.js). A card with a
|
||||
description opens .mod-detail — one shared panel outside the
|
||||
scroller, so it isn't clipped — which grows out of the card. */ ?>
|
||||
<div class="mod-strip">
|
||||
<button type="button" class="mod-nav mod-nav-prev" aria-label="Modalidades anteriores"><i class="fas fa-chevron-left" aria-hidden="true"></i></button>
|
||||
<div class="mod-scroll" tabindex="0" aria-label="Modalidades — desliza para ver mais">
|
||||
<?php foreach($modalidades as $d): $desc = trim(strip_tags((string) ($d['descricao'] ?? ''))) !== '' ? trim((string) $d['descricao']) : ''; ?>
|
||||
<div class="icon-box-item mod-card<?= $desc !== '' ? ' has-info' : '' ?>" data-color="<?= htmlspecialchars((string) $d['cor']) ?>"<?= $desc !== '' ? ' role="button" tabindex="0" aria-haspopup="dialog" aria-label="Ver descrição de ' . htmlspecialchars((string) $d['nome']) . '"' : '' ?>>
|
||||
<div class="ib-icon">
|
||||
<?= modalidade_icon_html($d['icon']) /* inline animated SVG for the FitMe set, else <img> */ ?>
|
||||
</div>
|
||||
<h3><?= htmlspecialchars((string) $d['nome']) ?></h3>
|
||||
<?php if ($desc !== ''): ?>
|
||||
<?php /* descricao is admin-authored rich text — sanitised allowlist HTML */ ?>
|
||||
<template class="mod-desc"><?= clean_richtext($desc) ?></template>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<button type="button" class="mod-nav mod-nav-next" aria-label="Mais modalidades"><i class="fas fa-chevron-right" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<div class="mod-detail" role="dialog" aria-modal="false" aria-hidden="true">
|
||||
<button type="button" class="md-close" aria-label="Fechar">×</button>
|
||||
<div class="md-card">
|
||||
<div class="ib-icon"></div>
|
||||
<h3></h3>
|
||||
</div>
|
||||
<div class="md-body">
|
||||
<strong class="md-name"></strong>
|
||||
<div class="md-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
// ── Equipa ──────────────────────────────────────────────────────────────────────
|
||||
if ($team):
|
||||
@@ -166,59 +239,6 @@ if ($team):
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
// ── Modalidades ─────────────────────────────────────────────────────────────────
|
||||
if ($modalidades):
|
||||
?>
|
||||
<section id="modalidades" class="service-section" aria-labelledby="modalidades-title">
|
||||
<div class="container">
|
||||
<div class="row justify-content-md-center mb-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="section-title text-center deco">
|
||||
<span class="section-eyebrow">Treinos</span>
|
||||
<h2 id="modalidades-title">Modalidades</h2>
|
||||
<p class="section-subtitle">Encontra a modalidade certa para ti.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php /* Full-width strip of modalidade cards. It is a real horizontal
|
||||
scroller (touch, trackpad/wheel, mouse drag, arrows) that also
|
||||
glides on its own until someone interacts (main.js). A card with a
|
||||
description opens .mod-detail — one shared panel outside the
|
||||
scroller, so it isn't clipped — which grows out of the card. */ ?>
|
||||
<div class="mod-strip">
|
||||
<button type="button" class="mod-nav mod-nav-prev" aria-label="Modalidades anteriores"><i class="fas fa-chevron-left" aria-hidden="true"></i></button>
|
||||
<div class="mod-scroll" tabindex="0" aria-label="Modalidades — desliza para ver mais">
|
||||
<?php foreach($modalidades as $d): $desc = trim(strip_tags((string) ($d['descricao'] ?? ''))) !== '' ? trim((string) $d['descricao']) : ''; ?>
|
||||
<div class="icon-box-item mod-card<?= $desc !== '' ? ' has-info' : '' ?>" data-color="<?= htmlspecialchars((string) $d['cor']) ?>"<?= $desc !== '' ? ' role="button" tabindex="0" aria-haspopup="dialog" aria-label="Ver descrição de ' . htmlspecialchars((string) $d['nome']) . '"' : '' ?>>
|
||||
<div class="ib-icon">
|
||||
<?= modalidade_icon_html($d['icon']) /* inline animated SVG for the FitMe set, else <img> */ ?>
|
||||
</div>
|
||||
<h3><?= htmlspecialchars((string) $d['nome']) ?></h3>
|
||||
<?php if ($desc !== ''): ?>
|
||||
<?php /* descricao is admin-authored rich text — sanitised allowlist HTML */ ?>
|
||||
<template class="mod-desc"><?= clean_richtext($desc) ?></template>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<button type="button" class="mod-nav mod-nav-next" aria-label="Mais modalidades"><i class="fas fa-chevron-right" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<div class="mod-detail" role="dialog" aria-modal="false" aria-hidden="true">
|
||||
<button type="button" class="md-close" aria-label="Fechar">×</button>
|
||||
<div class="md-card">
|
||||
<div class="ib-icon"></div>
|
||||
<h3></h3>
|
||||
</div>
|
||||
<div class="md-body">
|
||||
<strong class="md-name"></strong>
|
||||
<div class="md-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
// ── Horário ─────────────────────────────────────────────────────────────────────
|
||||
if ($horario):
|
||||
@@ -556,6 +576,7 @@ if (isset($_GET['assunto'])) {
|
||||
}
|
||||
$session = session();
|
||||
?>
|
||||
<?php if ($on('contactos')): ?>
|
||||
<?php /* contacts-bg.jpg never existed — outside2.jpg is the current photo in img/contactos/ */ ?>
|
||||
<section id="contactos" class="contact-section set-bg" data-setbg="<?= base_url('site/assets/img/contactos/outside2.jpg') ?>" aria-labelledby="contactos-title">
|
||||
<div class="container">
|
||||
@@ -663,6 +684,8 @@ $session = session();
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php endif; /* contactos */ ?>
|
||||
|
||||
<?php
|
||||
// ── Horário de Funcionamento ──────────────────────────────────────────────────
|
||||
if ($whours):
|
||||
@@ -694,6 +717,7 @@ usort($whours, fn($x, $y) => strcmp((string) $x['open'], (string) $y['open']));
|
||||
|
||||
<?php
|
||||
// ── Personal Trainer CTA ────────────────────────────────────────────────────────
|
||||
if ($on('personal_trainer')):
|
||||
?>
|
||||
<section id="personal-trainer" class="classes-section" aria-labelledby="pt-title">
|
||||
<div class="container">
|
||||
@@ -713,6 +737,8 @@ usort($whours, fn($x, $y) => strcmp((string) $x['open'], (string) $y['open']));
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php endif; /* personal trainer */ ?>
|
||||
|
||||
<?php
|
||||
// ── Aula experimental modal ─────────────────────────────────────────────────
|
||||
// Queues a booking request to id.andmore.pt (centralized mail) via site/agendar.php.
|
||||
@@ -726,7 +752,7 @@ usort($whours, fn($x, $y) => strcmp((string) $x['open'], (string) $y['open']));
|
||||
<p>Treina connosco sem compromisso. Deixa os teus dados e entramos em contacto para confirmar o dia e a hora.</p>
|
||||
<button type="button" class="trial-close" data-dismiss="modal" aria-label="Fechar">×</button>
|
||||
</div>
|
||||
<form class="trial-form" method="post" action="<?= base_url('home/agendar') ?>">
|
||||
<form class="trial-form" method="post" action="<?= htmlspecialchars(gym_url($gym, 'agendar')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<div class="form-row">
|
||||
<div class="col-12 col-md-6 mb-3">
|
||||
@@ -785,7 +811,7 @@ if ($memberships):
|
||||
<p>Deixa os teus contactos e entramos em contacto para concluir a inscrição.</p>
|
||||
<button type="button" class="trial-close" data-dismiss="modal" aria-label="Fechar">×</button>
|
||||
</div>
|
||||
<form class="trial-form" method="post" action="<?= base_url('home/inscricao') ?>">
|
||||
<form class="trial-form" method="post" action="<?= htmlspecialchars(gym_url($gym, 'inscricao')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="text" name="website" class="event-hp" tabindex="-1" autocomplete="off" aria-hidden="true">
|
||||
<div class="form-row">
|
||||
|
||||
+11
-4
@@ -10,15 +10,20 @@
|
||||
require_once __DIR__ . '/../config/db.php';
|
||||
require_once __DIR__ . '/../config/functions.php';
|
||||
|
||||
// The gym comes from the URL folder (/<slug>/inscricao); the old /home/inscricao
|
||||
// route has no ?gym and falls back to the default gym.
|
||||
$gym = site_gym($pdo) ?? db_gyms_default($pdo) ?? gym_fallback();
|
||||
$GLOBALS['_gym'] = $gym;
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
header('Location: ' . base_url('home#mensalidades'));
|
||||
header('Location: ' . gym_url($gym, '#mensalidades'));
|
||||
exit;
|
||||
}
|
||||
|
||||
csrf_check(); // 403s and exits on a bad/missing token
|
||||
|
||||
function inscricao_redirect(string $status): void {
|
||||
header('Location: ' . base_url('home?inscricao=' . $status . '#mensalidades'));
|
||||
header('Location: ' . gym_url(current_gym(), '?inscricao=' . $status . '#mensalidades'));
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -27,9 +32,11 @@ if (trim((string) ($_POST['website'] ?? '')) !== '') inscricao_redirect('ok');
|
||||
|
||||
$consent_text = 'Autorizo o contacto e o tratamento dos meus dados para efeitos desta inscrição, nos termos da Política de Privacidade.';
|
||||
$membership = db_memberships_get($pdo, (int) ($_POST['membership_id'] ?? 0));
|
||||
if ($membership && ($membership['deleted_at'] !== null || !$membership['is_active'])) $membership = null;
|
||||
if ($membership && ($membership['deleted_at'] !== null || !$membership['is_active']
|
||||
|| (int) $membership['gym_id'] !== (int) $gym['id'])) $membership = null;
|
||||
|
||||
$lead = [
|
||||
'gym_id' => (int) $gym['id'],
|
||||
'membership_id' => $membership ? (int) $membership['id'] : null,
|
||||
'membership_name' => $membership ? $membership['name'] : '',
|
||||
'name' => trim((string) ($_POST['nome'] ?? '')),
|
||||
@@ -58,7 +65,7 @@ try {
|
||||
|
||||
// Notify the gym. Best effort: the request is already stored.
|
||||
try {
|
||||
$info = site_info($pdo);
|
||||
$info = site_info($pdo, (int) $gym['id']);
|
||||
$to = $info['email'] !== '' ? $info['email'] : EMAIL_FROM;
|
||||
$e = static fn($v) => htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
|
||||
$rows = [
|
||||
|
||||
Reference in new issue
Block a user