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:
commit
e68d357177
2330 files changed
+215968
No files matched your search
@@ -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 -->
|
||||
@@ -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 -->
|
||||
@@ -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 -->
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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; ?>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user