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
+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">
|
||||
|
||||
Reference in new issue
Block a user