Initial import of fitme3070.pt platform

Plain-PHP fleet platform with id.andmore.pt SSO. Wires shared/platform-tools
as a submodule (platform_update, backups, git/migration tools) like the other
platforms.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
andreandClaude Opus 4.8 committed 2026-06-12 10:44:58 +01:00
commit e68d357177
2330 files changed
+215968

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
<!-- Classes section -->
<section class="classes-section" data-setbg="<?= isset($ad_footer_bg) ? $ad_footer_bg : ''; ?>">
<div class="container">
<div class="row">
<div class="col-lg-6">
<div class="section-title">
<h2><?= $ad_footer_h2 ?> </h2>
</div>
</div>
<div class="col-lg-6">
<div class="classes-text">
<p><?= $ad_footer_text ?> </p>
<a href="<?= base_url( $ad_footer_link ) ?>" class="site-btn"><?= $ad_footer_text_btn ?></a>
</div>
</div>
</div>
</div>
</section>
<!-- Trainers section end -->
+18
View File
@@ -0,0 +1,18 @@
<!-- Classes section -->
<section class="classes-section has-bg set-bg fade-in" data-setbg="<?= base_url('site/assets/img/crossfit/banner2.jpg') ?>">
<div class="container">
<div class="row">
<div class="col-lg-6">
<div class="section-title">
<h2>O que é o <span>CrossFit?</span></h2>
</div>
</div>
<div class="col-lg-6">
<div class="classes-text">
<p>CrossFit é um estilo de vida caracterizado pelo exercício físico, seguro e boa nutrição. O CrossFit pode ser usado para conseguir obter vários objetivos desde saúde, perda de peso e melhorias de performance. Funciona não só para quem está a iniciar como também para quem já treina há vários anos.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Trainers section end -->
+19
View File
@@ -0,0 +1,19 @@
<!-- Classes section -->
<section class="classes-section">
<div class="container">
<div class="row">
<div class="col-lg-6">
<div class="section-title">
<h2><a href="https://journal.crossfit.com" target="_blank" title="CrossFit Journal: The Performance-Based Lifestyle Resource"> <img loading="lazy" src="https://de45qwmlmgefw.cloudfront.net/badges/black-300x150.png" width="300px" height="150" alt="CrossFit Journal: The Performance-Based Lifestyle Resource" /></a></h2>
</div>
</div>
<div class="col-lg-6">
<div class="classes-text">
<p>Mais do que uma modalidade, o CrossFit é um estilo de vida. O CrossFit Journal tem as dicas essenciais desde alimentação, treinos que pode realizar em casa e as novidades da modalidade pelo mundo inteiro.
Mantenha-se informado e sempre a par das novidades sobre o mundo do CrossFit. </p>
<a href="https://journal.crossfit.com" class="site-btn">Visita o CrossFitJournal</a> </div>
</div>
</div>
</div>
</section>
<!-- Trainers section end -->
+40
View File
@@ -0,0 +1,40 @@
<!-- Footer section -->
<footer class="footer-section set-bg fade-in"
data-setbg="<?= base_url(($current_uri_2 ?? '') == 'crossfit' ? 'site/assets/img/footer-bg-cf.jpg' : 'site/assets/img/footer-bg-gym.jpg') ?>">
<?php /* Contact details live in the #contactos section — the footer just carries
social links, the site menu and the mandatory legal links. */ ?>
<div class="container text-center">
<?php if (!empty($info['facebook']) || !empty($info['instagram']) || !empty($info['whatsapp'])): ?>
<div class="social-links footer-social">
<?php if(!empty($info['facebook'])): ?>
<a href="<?= htmlspecialchars($info['facebook']) ?>" target="_blank" rel="noopener" aria-label="Facebook"><i class="fab fa-facebook"></i></a>
<?php endif; ?>
<?php if(!empty($info['instagram'])): ?>
<a href="<?= htmlspecialchars($info['instagram']) ?>" target="_blank" rel="noopener" aria-label="Instagram"><i class="fab fa-instagram"></i></a>
<?php endif; ?>
<?php if(!empty($info['whatsapp'])): ?>
<a target="_blank" href="https://wa.me/<?= htmlspecialchars($info['whatsapp']) ?>" rel="noopener" aria-label="WhatsApp"><i class="fab fa-whatsapp"></i></a>
<?php endif; ?>
</div>
<?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>
<?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>
<p class="small footer-legal"><a href="<?= base_url('home/privacidade') ?>">Política de Privacidade</a>
<?php /* Discreet backoffice entry point — bounces through SSO (id.andmore.pt). */ ?>
<span class="footer-legal-sep" aria-hidden="true">·</span>
<a href="<?= base_url('views/auth/login.php') ?>" class="footer-admin-link" rel="nofollow" aria-label="Acesso reservado" title="Backoffice"><i class="fas fa-lock"></i></a>
</p>
</div>
</footer>
<!-- Scripts: jQuery first (Bootstrap navbar/alerts depend on it), then plugins -->
<?= script_tag('site/assets/js/jquery-3.7.1.min.js') ?>
<?= script_tag('assets/vendor/bootstrap/js/bootstrap.min.js') ?>
<?= script_tag('site/assets/js/splide/dist/js/splide.min.js') ?>
<?= script_tag('site/assets/js/main.js') ?>
</body>
</html>
+46
View File
@@ -0,0 +1,46 @@
<?php
// Gallery partial — Splide CSS/JS is loaded centrally by include.php / footer.php.
// Slides use Splide's native lazy loading (data-splide-lazy + cover), so only
// the visible/nearby images are downloaded instead of the whole gallery.
$files = glob(rtrim($gallery_path, '/') . '/*.jpg');
if (!$files || !is_array($files)) $files = [];
?>
<div class="row justify-content-md-center" style="margin-top: 58px">
<div class="col-lg-8">
<div class="section-title text-center deco">
<span class="section-eyebrow">Galeria</span>
<h2><?= htmlspecialchars($gallery_h2 ?? 'Galeria') ?></h2>
<p class="section-subtitle">Espaço amplo, equipamento moderno e um ambiente feito para treinar.</p>
</div>
</div>
</div>
<div id="primary-slider" class="splide" style="padding:0;">
<div class="splide__track">
<ul class="splide__list">
<?php foreach($files as $f): ?>
<li class="splide__slide"><img src="<?= base_url($f) ?>" alt="Instalações FitMe 3070"></li>
<?php endforeach; ?>
</ul>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
var isMobile = window.innerWidth < 768;
// Single auto-playing slider (no thumbnail strip). Real <img src> + cover
// paints the first big image on mount; autoplay cross-fades through the
// rest and rewinds at the end. Pauses on hover/focus for accessibility.
new Splide('#primary-slider', {
type: 'fade',
rewind: true,
heightRatio: isMobile ? 0.7 : 0.5,
cover: true,
arrows: !isMobile,
pagination: true,
autoplay: true,
interval: 4500,
speed: 800,
pauseOnHover: true,
pauseOnFocus: true
}).mount();
});
</script>
+38
View File
@@ -0,0 +1,38 @@
<div class="clearfix"></div>
<div class="video fade-in">
<div class="overlay"></div>
<video playsinline autoplay muted loop poster="<?= base_url($header_video_img) ?>">
<source src="<?= base_url($header_video_video) ?>" type="video/mp4">
</video>
<div class="container h-100">
<div class="d-flex h-100 text-center align-items-center">
<div class="w-100 text-white">
<h1 class="display-3"><img src="<?= base_url('site/assets/img/logos/logo_crossfit_white.svg')?>" alt="CrossFit" style="opacity: .85; max-width: 320px;"></h1>
<p class="lead mb-0"></p>
</div>
</div>
</div>
</div>
<style>
.video {
position: relative; background-color: #000;
height: 75vh; min-height: 25rem; width: 100%;
overflow: hidden;
}
.video video {
position: absolute; top: 50%; left: 50%;
min-width: 100%; min-height: 100%;
width: auto; height: auto; z-index: 0;
transform: translate(-50%, -50%);
object-fit: cover;
}
.video .container { position: relative; z-index: 2; }
/* Fallback poster for touch devices */
@media (pointer: coarse) and (hover: none) {
.video {
background: url('<?= base_url($header_video_img) ?>') #000 center/cover no-repeat;
}
.video video { display: none; }
}
</style>
+173
View File
@@ -0,0 +1,173 @@
<?php
// Weekly schedule — time-aligned CSS grid with a single-day picker. Shared by
// the CrossFit page (the home page renders its own copy inline). $data holds the
// horario rows (site_horario), $schedule_h2 the heading. Mirrors the home grid
// so the same CSS (.schedule…) and the day-filter JS apply unchanged.
$week_day = ['Seg'=>'segunda','Ter'=>'terça','Qua'=>'quarta','Qui'=>'quinta','Sex'=>'sexta','Sab'=>'sábado'];
$rows = $data ?? [];
$week_values = array_values($week_day);
$today_map = [1=>'segunda', 2=>'terça', 3=>'quarta', 4=>'quinta', 5=>'sexta', 6=>'sábado'];
$default_day = $today_map[(int) date('N')] ?? null;
if (!in_array($default_day, $week_values, true)) { $default_day = $week_values[0]; }
// Distinct start times → grid rows; days → columns. Split Manhã (<12:00) / Tarde.
$slot_times = [];
foreach ($rows as $d) { $slot_times[$d['hora_inicio']] = true; }
ksort($slot_times);
$slot_times = array_keys($slot_times);
$morning = $afternoon = [];
foreach ($slot_times as $t) { if ((int) substr($t, 0, 2) < 12) { $morning[] = $t; } else { $afternoon[] = $t; } }
$row_of = []; $sections = []; $gr = 2; // row 1 = day headers
if ($morning) { $sections[] = ['Manhã', $gr++]; foreach ($morning as $t) { $row_of[$t] = $gr++; } }
if ($afternoon) { $sections[] = ['Tarde', $gr++]; foreach ($afternoon as $t) { $row_of[$t] = $gr++; } }
$total_rows = $gr - 1;
$grid = [];
foreach ($rows as $d) { $grid[$d['dia_semana']][$d['hora_inicio']][] = $d; }
?>
<div class="schedule fade-in">
<div class="container">
<?php if (!empty($schedule_h2)): ?>
<div class="row justify-content-md-center mb-4">
<div class="col-lg-8">
<div class="section-title text-center deco">
<h2><?= htmlspecialchars($schedule_h2) ?></h2>
</div>
<div class="text-center mb-2">
<button type="button" class="schedule-dl-btn" id="scheduleDownloadCf">
<i class="fas fa-download" aria-hidden="true"></i> Guardar horário (JPG)
</button>
</div>
</div>
</div>
<?php endif; ?>
<div class="schedule-day-filter" role="tablist" aria-label="Filtrar horário por dia">
<?php foreach ($week_day as $key => $value): ?>
<button type="button" class="sdf-btn<?= $value === $default_day ? ' active' : '' ?>" role="tab" data-day="<?= htmlspecialchars($value) ?>"><?= $key ?></button>
<?php endforeach; ?>
</div>
<div class="row">
<div class="col">
<div class="schedule_container">
<div class="schedule_panel" style="--sched-days:<?= count($week_day) ?>;--sched-rows:<?= $total_rows ?>;">
<?php $col = 0; foreach ($week_day as $key => $value): $col++; ?>
<div class="schedule_stripe<?= $col % 2 === 0 ? ' is-alt' : '' ?>" aria-hidden="true" style="grid-column:<?= $col ?>;grid-row:1 / -1;"></div>
<?php endforeach; ?>
<?php $col = 0; foreach ($week_day as $key => $value): $col++; ?>
<div class="schedule_dayhead<?= $value === $default_day ? '' : ' sched-hide' ?>" data-day="<?= htmlspecialchars($value) ?>" style="grid-column:<?= $col ?>;grid-row:1;">
<div class="rounded_frame"><?= $key ?></div>
</div>
<?php endforeach; ?>
<?php foreach ($sections as $sec): ?>
<div class="schedule_section_label" data-section="<?= $sec[0] === 'Manhã' ? 'manha' : 'tarde' ?>" style="grid-row:<?= $sec[1] ?>;"><span><?= htmlspecialchars($sec[0]) ?></span></div>
<?php endforeach; ?>
<?php $col = 0; foreach ($week_day as $key => $value): $col++; ?>
<?php foreach ($slot_times as $time): ?>
<?php if (!empty($grid[$value][$time])): ?>
<div class="schedule_cell<?= $value === $default_day ? '' : ' sched-hide' ?>" data-day="<?= htmlspecialchars($value) ?>" data-section="<?= ((int) substr($time, 0, 2) < 12) ? 'manha' : 'tarde' ?>" style="grid-column:<?= $col ?>;grid-row:<?= $row_of[$time] ?>;">
<?php foreach ($grid[$value][$time] as $d): ?>
<div class="schedule_box" style="background-color:<?= htmlspecialchars((string) $d['cor']) ?>">
<img src="<?= site_img_url($d['icon'], 'uploads/modalidades/icons') ?>" alt="<?= htmlspecialchars((string) $d['nome']) ?>">
<a><b><?= htmlspecialchars((string) $d['nome']) ?></b></a>
<div class="schedule_info">
<?= htmlspecialchars((string) $d['hora_inicio']) ?>
<b><?= htmlspecialchars((string) $d['local']) ?></b>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php endforeach; ?>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
/* Quick "save the weekly schedule as JPG" — replicated from the home page,
scoped to this partial's .schedule (the CrossFit page has no #horario wrapper).
Renders the live schedule in a 1920px virtual window and restyles the CLONE
(onclone) with .schedule-export so the image is always the full desktop week,
stamped with the generation date. */
(function () {
var btn = document.getElementById('scheduleDownloadCf');
var schedule = btn ? btn.closest('.schedule') : null;
if (!btn || !schedule) return;
var H2C_SRC = '<?= base_url('site/assets/js/print/html2canvas.min.js') ?>?v=1.4.1';
var BOARD_W = 1920;
function loadH2C() {
if (window.html2canvas) return Promise.resolve();
return new Promise(function (resolve, reject) {
var s = document.createElement('script');
s.src = H2C_SRC;
s.onload = resolve;
s.onerror = function () { reject(new Error('Falha a carregar html2canvas')); };
document.head.appendChild(s);
});
}
btn.addEventListener('click', function () {
var label = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = 'A gerar imagem…';
var fontsReady = (document.fonts && document.fonts.ready) ? document.fonts.ready : Promise.resolve();
loadH2C().then(function () { return fontsReady; }).then(function () {
var bg = (getComputedStyle(document.body).getPropertyValue('--fitme-surface') || '#F7F8FA').trim();
return html2canvas(schedule, {
backgroundColor: bg,
scale: 1,
useCORS: true,
logging: false,
windowWidth: BOARD_W,
windowHeight: 1080,
scrollX: 0,
scrollY: 0,
onclone: function (doc) {
var board = doc.querySelector('.schedule');
if (!board) return;
board.classList.add('schedule-export');
board.classList.remove('fade-in');
Array.prototype.forEach.call(board.querySelectorAll('.fade-in, .revealed'), function (el) {
el.classList.remove('fade-in');
el.classList.remove('revealed');
});
var f = board.querySelector('.schedule-day-filter'); if (f) f.parentNode.removeChild(f);
var b = board.querySelector('.schedule-dl-btn'); if (b) { var w = b.closest('div'); (w || b).parentNode.removeChild(w || b); }
Array.prototype.forEach.call(board.querySelectorAll('.sched-hide'), function (c) { c.classList.remove('sched-hide'); });
var stamp = doc.createElement('p');
stamp.className = 'schedule-stamp';
stamp.textContent = 'Gerado a ' + new Date().toLocaleDateString('pt-PT') + ' · sujeito a alterações';
(board.querySelector('.container') || board).appendChild(stamp);
}
});
}).then(function (canvas) {
return new Promise(function (resolve, reject) {
canvas.toBlob(function (blob) {
if (!blob) { reject(new Error('Falha ao gerar a imagem')); return; }
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'horario-crossfit-' + new Date().toISOString().slice(0, 10) + '.jpg';
document.body.appendChild(a); a.click(); document.body.removeChild(a);
setTimeout(function () { URL.revokeObjectURL(url); }, 1500);
resolve();
}, 'image/jpeg', 0.95);
});
}).catch(function (err) {
console.error(err);
alert('Não foi possível gerar a imagem. Tenta novamente.');
}).then(function () {
btn.disabled = false;
btn.innerHTML = label;
});
});
})();
</script>
+71
View File
@@ -0,0 +1,71 @@
<?php
// Per-page metadata, keyed by $current_uri_2. The canonical URL comes from
// base_url(), which matches the https + non-www redirect in .htaccess.
$page = $current_uri_2 ?? '';
$page_meta = [
'' => ['FitMe 3070 — Ginásio & CrossFit em Mira',
'Ginásio em Mira (Lagoa) com musculação, CrossFit, aulas de grupo, personal trainers, nutrição e fisioterapia. Inscreve-te já no FitMe 3070!'],
'crossfit' => ['CrossFit 3070 — Box de CrossFit em Mira | FitMe 3070',
'Box de CrossFit em Mira (Lagoa). Aulas para todos os níveis com treinadores certificados. Marca a tua aula experimental no CrossFit 3070!'],
'privacidade' => ['Política de Privacidade | FitMe 3070',
'Política de privacidade do FitMe 3070 — como recolhemos, usamos e protegemos os teus dados pessoais.'],
];
[$page_title, $page_desc] = $page_meta[$page] ?? $page_meta[''];
$canonical = base_url($page !== '' ? 'home/' . $page : '');
?><!doctype html>
<html lang="pt">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="<?= htmlspecialchars($page_desc) ?>">
<meta name="keywords" content="fitness, ginásio, exercício, personal trainer, crossfit, box, aveiro, mira">
<meta name="author" content="FitMe 3070">
<meta name="theme-color" content="#C2D500">
<title><?= htmlspecialchars($page_title) ?></title>
<link rel="canonical" href="<?= $canonical ?>">
<!-- Preconnect to external origins -->
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
<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') ?>">
<?php endif; ?>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700,900&display=swap" rel="stylesheet">
<!-- Vendor: Bootstrap 4.3.1 -->
<?= link_tag('assets/vendor/bootstrap/css/bootstrap.min.css') ?>
<!-- Vendor: Font Awesome 5 -->
<?= link_tag('site/assets/fonts/fontawesome/css/all.css') ?>
<!-- Vendor: Splide (gallery slider) — load BEFORE app styles so style.css wins -->
<link rel="stylesheet" href="<?= base_url('site/assets/js/splide/dist/css/splide.min.css') ?>">
<link rel="stylesheet" href="<?= base_url('site/assets/js/splide/dist/css/themes/splide-sea-green.min.css') ?>">
<!-- App styles (Bootstrap overrides) -->
<?= link_tag('site/assets/css/style.css') ?>
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="<?= base_url('site/assets/img/logos/logo_black.svg') ?>">
<link rel="apple-touch-icon" sizes="180x180" href="<?= base_url('site/assets/img/logos/apple-touch-icon.png') ?>">
<!-- Open Graph / link previews -->
<meta property="og:url" content="<?= $canonical ?>">
<meta property="og:type" content="website">
<meta property="og:site_name" content="FitMe 3070">
<meta property="og:locale" content="pt_PT">
<meta property="og:title" content="<?= htmlspecialchars($page_title) ?>">
<meta property="og:description" content="<?= htmlspecialchars($page_desc) ?>">
<meta property="og:image" content="<?= base_url('site/assets/img/og-image.jpg') ?>">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<!-- Structured data -->
<?= view('seo') ?>
</head>
<body>
<a href="#home" class="skip-link">Saltar para o conteúdo</a>
+10
View File
@@ -0,0 +1,10 @@
<div class="row justify-content-md-center">
<div class="col-lg-8">
<div class="section-title text-center">
<h2><?= isset($map_h2) ? $map_h2 : ''; ?></h2>
</div>
</div>
</div>
<div class="map" id="map">
<?= isset($info['google_maps']) ? $info['google_maps'] : ''; ?>
</div>
+9
View File
@@ -0,0 +1,9 @@
<?php
// Floating WhatsApp contact button (replaces the old Facebook Customer Chat
// plugin, which Meta discontinued in 2024). Styles live in style.css.
if (!empty($info['whatsapp'])): ?>
<a class="whatsapp-float" href="https://wa.me/<?= htmlspecialchars($info['whatsapp']) ?>"
target="_blank" rel="noopener" aria-label="Fale connosco no WhatsApp">
<i class="fab fa-whatsapp" aria-hidden="true"></i>
</a>
<?php endif; ?>
+155
View File
@@ -0,0 +1,155 @@
<?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="<?= base_url('home') ?>">
<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):
$is_crossfit = ($key === 'CrossFit');
$is_active = false;
if ($is_crossfit) {
$is_active = ($current_uri_2 === 'crossfit');
} elseif ($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="<?= base_url($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"' : '' ?>>
<?= $key ?>
</a>
<?php else: ?>
<a class="nav-link" href="<?= base_url($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() {
/* ── 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 });
updateActive();
}
})();
</script>
+35
View File
@@ -0,0 +1,35 @@
<section id="newsletter" class="newsletter-section set-bg fade-in" data-setbg="<?= base_url('site/assets/img/newsletter-bg/1.jpg') ?>">
<div class="container">
<div class="row">
<div class="col-lg-6">
<div class="section-title text-white mb-0">
<h2>Subscreve a nossa <span>newsletter</span></h2>
<p>Fica a par de todas as novidades e campanhas que temos para ti!</p>
<p class="small" style="opacity:.6;margin-top:-.5rem;">Junta-te a centenas de atletas que já recebem as nossas dicas e ofertas exclusivas.</p>
</div>
</div>
<div class="col-lg-6">
<form class="newsletter-form" method="post" action="<?= base_url('email/send_email_newsletter') ?>">
<input type="email" required name="newsletter-email" id="newsletter-email" placeholder="Escreve aqui o teu e-mail" aria-label="Endereço de e-mail">
<button class="site-btn" id="newsletter-btn" type="submit">Subscrever</button>
<label class="consent-check">
<input type="checkbox" required name="newsletter-consent" value="1">
<span>Li e aceito a <a href="<?= base_url('home/privacidade') ?>" target="_blank" rel="noopener">Política de Privacidade</a>.</span>
</label>
</form>
</div>
</div>
<?php
$session = session();
if ( $session->getFlashdata( 'newsletter' ) !== null ):
?>
<script>
document.getElementById('newsletter-email').value = <?= json_encode((string) $session->getFlashdata('newsletter'), JSON_HEX_TAG | JSON_HEX_AMP) ?>;
document.getElementById("newsletter-email").readOnly = true;
document.getElementById("newsletter-btn").disabled = true;
document.getElementById("newsletter-email").style.opacity = "0.7";
document.getElementById("newsletter-btn").style.opacity = "0.7";
</script>
<?php endif; ?>
</div>
</section>
+47
View File
@@ -0,0 +1,47 @@
<?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 = [
'@context' => 'https://schema.org',
'@type' => 'ExerciseGym',
'name' => ($info['name'] ?? '') !== '' ? $info['name'] : 'FitMe 3070',
'url' => base_url(),
'image' => base_url('site/assets/img/home.jpg'),
'logo' => base_url('site/assets/img/logos/logo_black.svg'),
];
if (!empty($info['phone'])) $ld['telephone'] = $info['phone'];
if (!empty($info['email'])) $ld['email'] = $info['email'];
if (!empty($info['adress'])) {
$ld['address'] = [
'@type' => 'PostalAddress',
'streetAddress' => $info['adress'],
'addressLocality' => 'Mira',
'addressCountry' => 'PT',
];
}
if (!empty($info['google_maps_link'])) $ld['hasMap'] = $info['google_maps_link'];
$ld_same_as = array_values(array_filter([$info['facebook'] ?? '', $info['instagram'] ?? '']));
if ($ld_same_as) $ld['sameAs'] = $ld_same_as;
// Opening hours: the 'segunda' row in work_hours covers Mon-Fri (see index.php).
$ld_days = [
'segunda' => ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
'sabado' => ['Saturday'],
'domingo' => ['Sunday'],
];
$ld_hours = [];
foreach ((isset($GLOBALS['pdo']) ? site_work_hours($GLOBALS['pdo']) : []) as $h) {
if (!isset($ld_days[$h['week_day']])) continue;
$ld_hours[] = [
'@type' => 'OpeningHoursSpecification',
'dayOfWeek' => $ld_days[$h['week_day']],
'opens' => substr((string) $h['open'], 0, 5),
'closes' => substr((string) $h['close'], 0, 5),
];
}
if ($ld_hours) $ld['openingHoursSpecification'] = $ld_hours;
?>
<script type="application/ld+json">
<?= json_encode($ld, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) ?>
</script>