Files
fitme3070.pt/site/includes/navbar.php
T

225 lines
11 KiB
PHP

<?php
if($current_uri_2 == 'crossfit'){
$logo_rel = 'site/assets/img/logos/logo_crossfit.svg';
}
else
{
// Each gym shows its own logo (FitMe In on /mira/, FitMe 3070 on /3070/).
$logo_rel = gym_logo(current_gym()) ?: 'site/assets/img/logos/logo_black.svg';
}
$logo = base_url($logo_rel);
$logo_dims = svg_img_dims_attr(dirname(__DIR__, 2) . '/' . $logo_rel);
// Home page: the logo starts big and centred over the hero (.is-top) and docks
// back to the left of the bar once the page scrolls (script below).
$center_logo = !empty($nav_center_logo);
?>
<header class="header-section lazy<?= $center_logo ? ' has-center-logo is-top' : '' ?>">
<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())) ?>">
<?php if (img_fallback_rel($logo_rel) !== ''): ?>
<?= img_tag($logo_rel, ['loading' => 'eager', 'decoding' => 'async', 'alt' => 'Logo ' . (current_gym()['name'] ?? 'FitMe 3070')]) ?>
<?php else: ?>
<img loading="eager" decoding="async" alt="Logo <?= htmlspecialchars(current_gym()['name'] ?? 'FitMe 3070') ?>" src="<?= htmlspecialchars($logo) ?>" <?= $logo_dims ?>>
<?php endif; ?>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-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>
<?php
// Other published gyms: the main gym opens first, the rest are
// highlighted buttons at the right end of the bar.
$other_gyms = [];
if (!empty($GLOBALS['pdo'])) {
foreach (db_gyms_published($GLOBALS['pdo']) as $g) {
if ((int) $g['id'] !== (int) current_gym()['id']) $other_gyms[] = $g;
}
}
if ($other_gyms): ?>
<div class="gym-switch d-flex flex-wrap gap-2 ms-lg-auto py-2 py-lg-0 pe-lg-3">
<?php foreach ($other_gyms as $g): ?>
<?php $g_logo = gym_logo($g); ?>
<a class="gym-switch-btn<?= $g_logo !== '' ? ' gym-switch-btn--logo' : '' ?>" href="<?= htmlspecialchars(gym_url($g)) ?>" style="<?= htmlspecialchars(gym_brand_vars($g)) ?>">
<?php if ($g_logo !== ''): ?>
<?= img_tag($g_logo, ['class' => 'gym-switch-logo', 'alt' => $g['name'], 'decoding' => 'async', 'width' => null]) ?>
<?php else: ?>
<span><?= htmlspecialchars($g['name']) ?></span>
<?php endif; ?>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</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() {
/* ── Centred logo: docks into the bar once the page leaves the top ───── */
var header = document.querySelector('.header-section.has-center-logo');
if (header) {
var dock = function() { header.classList.toggle('is-top', window.scrollY < 40); };
window.addEventListener('scroll', dock, { passive: true });
dock();
}
/* ── 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();
openHorarioIfNeeded(url.hash);
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();
}
});
});
/* Horário de Aulas is a closed accordion (button "Ver Horário das aulas") —
clicking the navbar link (or landing on #horario directly) should open
it right away instead of scrolling to a section that still needs an
extra click. */
function openHorarioIfNeeded(hash) {
if (hash !== '#horario') return;
var el = document.getElementById('horarioCollapse');
if (el && window.bootstrap) {
bootstrap.Collapse.getOrCreateInstance(el, { toggle: false }).show();
}
}
openHorarioIfNeeded(window.location.hash);
/* ── 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>