Files
fitme3070.pt/site/includes/navbar.php
T
andreandClaude Opus 5.5 5e300fc341 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>
2026-09-25 07:26:40 +01:00

170 lines
7.8 KiB
PHP

<?php
if($current_uri_2 == 'crossfit'){
$logo = base_url('site/assets/img/logos/logo_crossfit.svg');
}
else
{
$logo = base_url('site/assets/img/logos/logo_black.svg');
}
?>
<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="<?= 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 (str_ends_with($n, '#home')) {
// Only the Home link is active by default on the homepage
$is_active = ($current_uri_2 === '');
}
// Other anchor links activate via JS scroll spy, not PHP
?>
<li class="nav-item <?= $is_active ? 'active' : '' ?> main-menu-li" >
<?php if ($is_crossfit): ?>
<a class="nav-link crossfit-nav-link" href="<?= htmlspecialchars($n) ?>"<?= $is_active ? ' aria-current="page"' : '' ?>>
<?= $key ?>
</a>
<?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="<?= htmlspecialchars($n) ?>">
<?= $key ?>
</a>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>
</nav>
</header>
<div class="clearfix"></div>
<!-- Header section end -->
<!-- Smooth scroll + scroll-spy for one-page layout -->
<script>
(function() {
// This partial is printed by _open.php BEFORE the page sections (#equipa, …)
// exist in the DOM, so binding must wait for DOMContentLoaded — otherwise
// querySelector('#equipa') returns null, `sections` stays empty, and neither
// the smooth-scroll nor the scroll-spy (active nav link) ever bind.
function init() {
/* ── Eased smooth-scroll helper ──────────────────────────────────────── */
function smoothScrollTo(targetEl, duration) {
var start = window.scrollY;
var navbar = document.querySelector('.header-section');
var offset = navbar ? navbar.offsetHeight : 60;
var startTime = null;
duration = duration || 500;
function easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
}
function step(currentTime) {
if (!startTime) startTime = currentTime;
var elapsed = currentTime - startTime;
var progress = Math.min(elapsed / duration, 1);
// Recompute the destination every frame. These sections use
// content-visibility:auto, so off-screen ones report a placeholder
// height until rendered; a target computed once on click lands short
// (e.g. Contactos → Horário on the first click). The live absolute
// top (rect.top + scrollY) updates as sections render, so the scroll
// converges on the real position.
var end = targetEl.getBoundingClientRect().top + window.scrollY - offset;
window.scrollTo(0, start + (end - start) * easeInOutCubic(progress));
if (progress < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
/* ── Bind all smooth-scroll links ───────────────────────────────────── */
// Hrefs come from base_url() as absolute URLs (https://…/#equipa), so
// resolve them and only bind the ones whose hash targets THIS page.
// "/", "/home" and "/index.php" are all the same route.
function normPath(p) {
p = p.replace(/\/+$/, '');
if (p.slice(-10) === '/index.php') p = p.slice(0, -10);
if (p.slice(-5) === '/home') p = p.slice(0, -5);
return p;
}
var navLinks = document.querySelectorAll('a.smooth-scroll');
var sections = [];
navLinks.forEach(function(link) {
var url;
try { url = new URL(link.getAttribute('href'), window.location.href); } catch (e) { return; }
if (!url.hash || normPath(url.pathname) !== normPath(window.location.pathname)) return;
var target = document.querySelector(url.hash);
if (!target) return;
sections.push({ link: link, target: target, id: url.hash });
link.addEventListener('click', function(e) {
e.preventDefault();
smoothScrollTo(target, 600);
history.pushState(null, null, url.hash);
var navCollapse = document.getElementById('navbarSupportedContent');
if (navCollapse && navCollapse.classList.contains('show')) {
document.querySelector('.navbar-toggler').click();
}
});
});
/* ── Scroll-spy: highlight current section ──────────────────────────── */
// Measure each section against the viewport with getBoundingClientRect:
// a link is "current" only while the line just below the navbar sits
// *inside* its section (top at/above the line, bottom below it). We use the
// live rect rather than offsetTop/offsetHeight because these sections use
// `content-visibility:auto`, which makes those offset metrics unreliable.
// Bounds (not "top has passed") also means the offer + gallery gap above
// #equipa highlights nothing, so Home stops underlining past the header.
function updateActive() {
var navbarH = (document.querySelector('.header-section') || {}).offsetHeight || 60;
var line = navbarH + 12;
var current = null;
sections.forEach(function(s) {
var r = s.target.getBoundingClientRect();
if (r.top <= line && r.bottom > line) current = s;
});
document.querySelectorAll('.main-menu-li').forEach(function(li) {
li.classList.remove('active');
});
sections.forEach(function(s) {
s.link.removeAttribute('aria-current');
});
if (current) {
// Several links can point at the same section (nav item + mobile
// CTA); highlight every one that has a surrounding nav item.
sections.forEach(function(s) {
if (s.target !== current.target) return;
var li = s.link.closest('.nav-item');
if (li) li.classList.add('active');
s.link.setAttribute('aria-current', 'page');
});
}
}
// Only run the spy when this page actually has bound sections — on the
// CrossFit page the list is empty and the PHP-set active pill must stay.
if (sections.length > 0) {
window.addEventListener('scroll', updateActive, { passive: true });
window.addEventListener('resize', updateActive, { passive: true });
updateActive();
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>