Files
fitme3070.pt/site/index.php
T

735 lines
43 KiB
PHP

<?php
// One-page site: Home + Equipa + Modalidades + Horário + Contactos.
// CrossFit is a separate page (site/crossfit.php).
$current_uri_2 = '';
// 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 main (default) gym — the others are reached
// from the navbar's «Ginásios» menu.
if (($_GET['gym'] ?? '') === '') {
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../config/functions.php';
$published = db_gyms_published($pdo);
$main = null;
foreach ($published as $g) { if ((int) ($g['is_default'] ?? 0) === 1) { $main = $g; break; } }
$to = $main ?? $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 ───────────────────────────────────────────────────────────────
// 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) : [];
$work_hours_days = ['Segunda-Sexta'=>'segunda','Sábado'=>'sabado','Domingo/Feriados'=>'domingo'];
usort($whours, fn($x, $y) => strcmp((string) $x['open'], (string) $y['open'])); // morning before afternoon
$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 ────────────────────────────────────────────────────────────────
?>
<!-- Mobile sticky CTA bar -->
<div class="mobile-cta-bar">
<a href="tel:<?= htmlspecialchars(site_tel($info['phone'] ?? '')) ?>" class="cta-btn cta-btn-call"><i class="fas fa-phone-alt"></i> Ligar</a>
<a href="#contactos" class="cta-btn cta-btn-cta smooth-scroll">Inscreve-te Já!</a>
</div>
<!-- Back to top -->
<button class="back-to-top" id="backToTop" aria-label="Voltar ao topo" title="Voltar ao topo">&uarr;</button>
<!-- 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('<?= 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
à sua landing page em /evento/<slug>. */ ?>
<?php if ($events): ?>
<div class="hero-events">
<?php foreach ($events as $ev):
$ac = $ev['cor'] !== '' ? $ev['cor'] : '#C2D500';
$ev_img = $ev['hero_image'] !== '' ? base_url('uploads/events/' . rawurlencode($ev['hero_image'])) : '';
?>
<a class="hero-event<?= $ev_img ? ' has-img' : '' ?>" href="<?= base_url('evento/' . rawurlencode($ev['slug'])) ?>"
style="--ev-accent:<?= htmlspecialchars($ac, ENT_QUOTES) ?><?= $ev_img ? ';--ev-img:url(\'' . htmlspecialchars($ev_img, ENT_QUOTES) . '\')' : '' ?>">
<span class="hero-event-badge"><span class="event-live-dot"></span><i class="fas fa-bolt"></i> Evento</span>
<span class="hero-event-text">
<strong class="hero-event-title"><?= htmlspecialchars($ev['title']) ?></strong>
<?php if ($ev['summary'] !== ''): ?><span class="hero-event-sub"><?= htmlspecialchars($ev['summary']) ?></span><?php endif; ?>
</span>
<span class="hero-event-go">Saber mais <i class="fas fa-arrow-right"></i></span>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<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="hero-text-link" data-bs-toggle="modal" data-bs-target="#trialModal">Marca uma aula experimental</button>
</div>
</div>
</div>
</section>
<?php
// ── Oferta (Feature section) ───────────────────────────────────────────────────
if ($offer):
?>
<section id="oferta" class="feature-section" aria-labelledby="oferta-title">
<div class="container">
<div class="row justify-content-md-center">
<div class="col-lg-8">
<div class="section-title text-center deco">
<span class="section-eyebrow">FitMe 3070</span>
<h2 id="oferta-title">A Nossa Oferta</h2>
<p class="section-subtitle">Tudo o que precisas para evoluir, num só espaço.</p>
</div>
</div>
</div>
<div class="row auto-grid">
<?php foreach($offer as $o): ?>
<div class="col-6 col-sm-4 col-lg-3">
<div class="icon-box-item">
<div class="ib-icon">
<img loading="lazy" decoding="async" <?= site_img_dims_attr($o['o_img'], 'uploads/website/offer') ?> src="<?= htmlspecialchars(site_img_url($o['o_img'], 'uploads/website/offer')) ?>" alt="<?= htmlspecialchars((string) $o['o_title']) ?>">
</div>
<h3><?= htmlspecialchars((string) $o['o_title']) ?></h3>
<p><?= htmlspecialchars((string) $o['o_description']) ?></p>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</section>
<?php endif; ?>
<?php
// ── Galeria (Instalações) ───────────────────────────────────────────────────────
$gallery_path = !empty($gym['gallery_id']) ? 'uploads/gallery/' . (int) $gym['gallery_id'] . '/' : '';
$gallery_files = ($on('galeria') && $gallery_path !== '') ? preg_grep('/\.(jpe?g|png|gif|webp|jxl)$/i', glob($gallery_path . '*') ?: []) : [];
if ($gallery_files && count($gallery_files) > 0):
$gallery_h2 = 'Conhece as nossas instalações';
?>
<section id="galeria" class="gallery-section" style="padding:70px 0 0;background:var(--fitme-white)">
<?php include __DIR__ . '/includes/gallery.php'; ?>
</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">&times;</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):
?>
<section id="equipa" class="trainers-section" aria-labelledby="equipa-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">Quem somos</span>
<h2 id="equipa-title">Conhece os membros da equipa 3070</h2>
<p class="section-subtitle">Profissionais dedicados a ajudar-te a alcançar os teus objetivos.</p>
</div>
</div>
</div>
<div class="section-title text-center">
<div class="row auto-grid" id="powerRotation">
<?php foreach ($team as $d): if ($d['status'] == 'ativo'):
// Default card: photo + name + title. The horizontal detail card
// (hover on desktop, tap on touch) only exists when there is
// something extra to show, so a bare member stays a plain card.
$t_title = trim((string) ($d['title'] ?? ''));
$t_skills = text_lines($d['skills'] ?? '');
$t_certs = text_lines($d['certifications'] ?? '');
$t_bio = trim(strip_tags((string) ($d['description'] ?? ''))) !== '' ? clean_richtext((string) $d['description']) : '';
$t_detail = $t_skills || $t_certs || $t_bio !== '';
$t_img = htmlspecialchars(site_img_url($d['photo'], 'uploads/team/foto'));
$t_dims = site_img_dims_attr($d['photo'], 'uploads/team/foto');
$t_id = 'td-' . (int) $d['id'];
?>
<div class="col-6 col-md-4 col-lg-3" style="margin-top: 20px;">
<div class="trainer-item<?= $t_detail ? ' has-detail' : '' ?>"<?= $t_detail ? ' tabindex="0" role="button" aria-expanded="false" aria-controls="' . $t_id . '" aria-label="Ver detalhes de ' . htmlspecialchars((string) $d['name']) . '"' : '' ?>>
<div class="trainer-pic">
<img loading="lazy" decoding="async" <?= $t_dims ?> src="<?= $t_img ?>" alt="<?= htmlspecialchars((string) $d['name']) ?>">
</div>
<h3><?= htmlspecialchars((string) $d['name']) ?></h3>
<?php if ($t_title !== ''): ?><p class="trainer-title"><?= htmlspecialchars($t_title) ?></p><?php endif; ?>
<?php if ($t_detail): ?>
<div class="trainer-detail" id="<?= $t_id ?>">
<button type="button" class="td-close" aria-label="Fechar">&times;</button>
<div class="td-photo"><img loading="lazy" decoding="async" <?= $t_dims ?> src="<?= $t_img ?>" alt=""></div>
<div class="td-body">
<strong class="td-name"><?= htmlspecialchars((string) $d['name']) ?></strong>
<?php if ($t_title !== ''): ?><span class="td-title"><?= htmlspecialchars($t_title) ?></span><?php endif; ?>
<?php if ($t_bio !== ''): ?><div class="td-bio"><?= $t_bio ?></div><?php endif; ?>
<?php if ($t_skills): ?>
<div class="td-block"><span class="td-label">Aptidões / Funções</span>
<ul><?php foreach ($t_skills as $x): ?><li><?= htmlspecialchars($x) ?></li><?php endforeach; ?></ul></div>
<?php endif; ?>
<?php if ($t_certs): ?>
<div class="td-block"><span class="td-label">Formações</span>
<ul><?php foreach ($t_certs as $x): ?><li><?= htmlspecialchars($x) ?></li><?php endforeach; ?></ul></div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; endforeach; ?>
</div>
</div>
</div>
</section>
<?php endif; ?>
<?php
// ── Horário ─────────────────────────────────────────────────────────────────────
if ($horario):
$week_day = ['Seg'=>'segunda','Ter'=>'terça','Qua'=>'quarta','Qui'=>'quinta','Sex'=>'sexta','Sab'=>'sábado'];
// The schedule shows one day at a time on screen; default to today (or the first
// listed day if today isn't in the table, e.g. Sunday). Rendered server-side too
// so there's no flash of all days before the JS runs, and so it still works with
// JavaScript off. The JPG export clears these to show the whole week.
$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]; }
?>
<section id="horario" class="schedule-section" style="padding:60px 0;" aria-labelledby="horario-title">
<div class="schedule fade-in">
<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">Calendário</span>
<h2 id="horario-title">Horário de Aulas</h2>
<p class="section-subtitle">Planeia a tua semana de treinos connosco.</p>
</div>
<div class="text-center">
<button type="button" class="site-btn" data-bs-toggle="collapse" data-bs-target="#horarioCollapse" aria-expanded="false" aria-controls="horarioCollapse">
Ver Horário das aulas
</button>
</div>
</div>
</div>
<div class="collapse" id="horarioCollapse">
<div class="text-center mb-4">
<button type="button" class="schedule-dl-btn" id="scheduleDownload">
<i class="fas fa-download" aria-hidden="true"></i> Guardar horário
</button>
</div>
<?php
// ── Build a time-aligned grid ──────────────────────────────────────────
// Distinct start times become CSS-grid rows and days become columns, so
// classes that start at the same time line up on the same line across the
// week (e.g. every 07:00 CrossFit sits on one row, and Saturday's 09:15
// Cycling drops to the 09:15 row instead of riding up with the 07:00s).
// A single (day, time) cell can hold several classes — different rooms run
// concurrently (e.g. 19:15 CrossFit + FBB + YOURFIT PUMP) — and they sit
// side by side inside the cell so the overlap reads at a glance.
$slot_times = [];
foreach ($horario as $d) { $slot_times[$d['hora_inicio']] = true; }
ksort($slot_times); // 'HH:MM' strings sort chronologically
$slot_times = array_keys($slot_times);
// Split the day into Manhã (before 12:00) and Tarde (12:00 onward), then
// assign each piece a CSS-grid row: row 1 is the day headers, then a
// full-width section label precedes each non-empty block of time slots.
$morning = $afternoon = [];
foreach ($slot_times as $t) {
if ((int) substr($t, 0, 2) < 12) { $morning[] = $t; } else { $afternoon[] = $t; }
}
$row_of = []; // 'HH:MM' => grid-row
$sections = []; // [label, grid-row] for the divider rows
$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 = []; // $grid[dia_semana][hora_inicio] = [classes…]
foreach ($horario as $d) {
$grid[$d['dia_semana']][$d['hora_inicio']][] = $d;
}
?>
<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 // Full-height background band per day, painted behind the cards so each
// weekday column reads as its own vertical stripe. Emitted first so it
// sits underneath; alternating columns get a slightly deeper tint. ?>
<?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;">
<button type="button" class="sched-nav sched-prev" aria-label="Dia anterior"><i class="fas fa-chevron-left" aria-hidden="true"></i></button>
<div class="rounded_frame"><?= $key ?></div>
<button type="button" class="sched-nav sched-next" aria-label="Dia seguinte"><i class="fas fa-chevron-right" aria-hidden="true"></i></button>
</div>
<?php endforeach; ?>
<?php // Manhã / Tarde dividers spanning the full width above each block. ?>
<?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="<?= htmlspecialchars(site_img_url($d['icon'], 'uploads/modalidades/icons')) ?>" alt="<?= htmlspecialchars((string) $d['nome']) ?>" <?= site_img_dims_attr($d['icon'], 'uploads/modalidades/icons') ?> decoding="async">
<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>
<p class="schedule-scroll-hint">&larr; Deslize para ver todos os dias &rarr;</p>
</div>
</div>
</div>
</section>
<script>
/* Quick "save the weekly schedule as JPG". html2canvas (1.4.1, loaded lazily
on first use) re-renders the page inside a hidden iframe sized by
`windowWidth`, so we capture the live schedule but restyle its CLONE
(onclone) with .schedule-export and render in a 1920px virtual window:
desktop media queries apply there, so the image is always the full desktop
week at exactly 1920px wide — even when saved from a phone. The date stamp
makes a saved image obviously dated if the schedule changes later. */
(function () {
var btn = document.getElementById('scheduleDownload');
var schedule = document.querySelector('#horario .schedule');
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, // 1 CSS px = 1 canvas px → image is exactly 1920px wide
useCORS: true,
logging: false,
windowWidth: BOARD_W, // desktop-sized virtual window → desktop media queries
windowHeight: 1080,
scrollX: 0,
scrollY: 0,
onclone: function (doc) {
var board = doc.querySelector('#horario .schedule');
if (!board) return;
board.classList.add('schedule-export');
// The #horario section uses `content-visibility:auto`, which makes
// the browser skip rendering its contents when it considers them
// off-screen — inside the capture iframe that yields a blank image.
// Force it visible (and drop the intrinsic-size placeholder) so the
// grid actually paints.
var sec = doc.getElementById('horario');
if (sec) {
sec.style.contentVisibility = 'visible';
sec.style.containIntrinsicSize = 'auto';
sec.style.contain = 'none';
}
// The .fade-in / .revealed entry animations replay from
// their opacity:0 keyframe inside the capture iframe and
// would snapshot as a blank white image — strip them (the
// .schedule-export CSS also freezes opacity as a backstop).
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 b = doc.getElementById('scheduleDownload'); if (b) { var w = b.closest('div'); (w || b).parentNode.removeChild(w || b); }
// Un-hide every day (single-day mode may have hidden 5 of 6).
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-fitme3070-' + 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>
<?php endif; ?>
<?php
// ── Mensalidades ────────────────────────────────────────────────────────────────
// Cards come from the backoffice (Gestão → Mensalidades); one may be featured.
// "Inscrever" opens #joinModal pre-set to that card (site/inscricao.php stores
// the request as a lead and emails the gym).
if ($memberships):
?>
<section id="mensalidades" class="pricing-section" aria-labelledby="mensalidades-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">Mensalidades</span>
<h2 id="mensalidades-title">Escolhe o teu plano</h2>
<p class="section-subtitle">Encontra a mensalidade que encaixa na tua rotina.</p>
</div>
</div>
</div>
<?php if (($_GET['inscricao'] ?? '') === 'ok'): ?>
<div class="alert alert-success text-center" role="alert">Pedido de inscrição enviado! Vamos entrar em contacto contigo muito em breve.</div>
<?php elseif (($_GET['inscricao'] ?? '') === 'erro'): ?>
<div class="alert alert-danger text-center" role="alert">Não foi possível enviar o pedido. Verifica os dados e tenta novamente.</div>
<?php endif; ?>
<div class="pricing-grid">
<?php foreach ($memberships as $m): $features = text_lines($m['features']); ?>
<article class="price-card<?= $m['is_featured'] ? ' is-featured' : '' ?>">
<?php if ($m['is_featured']): ?><span class="pc-badge"><i class="fas fa-star"></i> <?= htmlspecialchars($m['featured_label']) ?></span><?php endif; ?>
<h3 class="pc-name"><?= htmlspecialchars($m['name']) ?></h3>
<?php if ($m['subtitle'] !== ''): ?><p class="pc-sub"><?= htmlspecialchars($m['subtitle']) ?></p><?php endif; ?>
<div class="pc-price">
<span class="pc-amount<?= $m['price'] === null ? ' is-text' : '' ?>"><?= htmlspecialchars(membership_price_label($m)) ?></span>
<?php if ($m['price'] !== null && $m['price_suffix'] !== ''): ?><span class="pc-suffix"><?= htmlspecialchars($m['price_suffix']) ?></span><?php endif; ?>
</div>
<?php if ($features): ?>
<ul class="pc-features">
<?php foreach ($features as $f): ?><li><i class="fas fa-check" aria-hidden="true"></i><?= htmlspecialchars($f) ?></li><?php endforeach; ?>
</ul>
<?php endif; ?>
<button type="button" class="site-btn pc-cta" data-bs-toggle="modal" data-bs-target="#joinModal"
data-membership="<?= (int) $m['id'] ?>"><?= htmlspecialchars($m['cta_label']) ?></button>
</article>
<?php endforeach; ?>
</div>
</div>
</section>
<?php endif; ?>
<?php
// ── Reviews Google ──────────────────────────────────────────────────────────────
// Live from the Places API (functions_reviews.php), cached; only reviews at or
// above the backoffice minimum rating. Hidden when nothing qualifies.
if ($reviews):
$stars = static function (float $r): string {
$out = '';
for ($i = 1; $i <= 5; $i++) {
$cls = $r >= $i ? 'fas fa-star' : ($r >= $i - 0.5 ? 'fas fa-star-half-alt' : 'far fa-star');
$out .= '<i class="' . $cls . '" aria-hidden="true"></i>';
}
return $out;
};
?>
<section id="reviews" class="reviews-section" aria-labelledby="reviews-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">Reviews</span>
<h2 id="reviews-title">O que dizem de nós</h2>
<?php if ($reviews['rating'] !== null): ?>
<p class="reviews-summary">
<strong><?= number_format((float) $reviews['rating'], 1, ',', '') ?></strong>
<span class="rv-stars" role="img" aria-label="<?= number_format((float) $reviews['rating'], 1, ',', '') ?> de 5 estrelas"><?= $stars((float) $reviews['rating']) ?></span>
<span><?= (int) $reviews['total'] ?> avaliações no Google</span>
</p>
<?php endif; ?>
</div>
</div>
</div>
<div class="reviews-grid">
<?php foreach ($reviews['reviews'] as $r): ?>
<figure class="review-card">
<?php // Reviewer first: big photo (or initial), name, date, stars — then the text, smaller. ?>
<div class="rv-avatar">
<?php $rv_img = review_avatar_url($r); /* guardada no próprio site (config/functions_privacy.php) */ ?>
<?php if ($rv_img !== ''): ?><img src="<?= htmlspecialchars($rv_img) ?>" alt="" loading="lazy" decoding="async" width="84" height="84">
<?php else: ?><span aria-hidden="true"><?= htmlspecialchars(mb_strtoupper(mb_substr($r['author'], 0, 1))) ?></span><?php endif; ?>
</div>
<figcaption class="rv-author">
<?php if ($r['author_uri'] !== ''): ?><a href="<?= htmlspecialchars($r['author_uri']) ?>" target="_blank" rel="noopener nofollow"><?= htmlspecialchars($r['author']) ?></a><?php else: ?><?= htmlspecialchars($r['author']) ?><?php endif; ?>
<small><?= htmlspecialchars($r['relative']) ?></small>
</figcaption>
<div class="rv-stars rv-stars-lg" role="img" aria-label="<?= (int) $r['rating'] ?> de 5 estrelas"><?= $stars((float) $r['rating']) ?></div>
<blockquote class="rv-text"><?= nl2br(htmlspecialchars($r['text'])) ?></blockquote>
</figure>
<?php endforeach; ?>
</div>
<div class="reviews-actions">
<?php if ($reviews['maps_uri'] !== ''): ?><a class="reviews-link" href="<?= htmlspecialchars($reviews['maps_uri']) ?>" target="_blank" rel="noopener">Ver todas no Google</a><?php endif; ?>
<a class="site-btn" href="<?= htmlspecialchars($reviews['write_uri']) ?>" target="_blank" rel="noopener">Deixa a tua review</a>
</div>
<p class="reviews-attribution">Reviews via Google Maps</p>
</div>
</section>
<?php endif; ?>
<?php
// ── Contactos + Horário de Funcionamento + Mapa ────────────────────────────────
$assunto = '';
if (isset($_GET['assunto'])) {
$assunto = $_GET['assunto'];
if ($assunto == 'pt'): $assunto = 'Informações - Personal Trainer';
elseif ($assunto == 'pt_cf'): $assunto = 'Informações - Personal Trainer CrossFit';
elseif ($assunto == 'insc'): $assunto = 'Informações - Inscrição no Ginásio';
elseif ($assunto == 'aula'): $assunto = 'Informações - Aula Experimental';
elseif ($assunto == 'nut'): $assunto = 'Informações - Nutricionista';
elseif ($assunto == 'fis'): $assunto = 'Informações - Fisioterapia';
endif;
}
echo view('contactos', [
'whours' => $whours,
'work_hours_days' => $work_hours_days,
'assunto' => $assunto,
'show' => $on('contactos'),
]);
// ── Personal Trainer CTA ────────────────────────────────────────────────────────
if ($on('personal_trainer')):
?>
<section id="personal-trainer" class="classes-section" aria-labelledby="pt-title">
<div class="container">
<div class="row">
<div class="col-lg-6">
<div class="section-title">
<h2 id="pt-title">Precisas de um <span>Personal Trainer</span>?</h2>
</div>
</div>
<div class="col-lg-6">
<div class="classes-text">
<p>Melhor do que treinar sozinho é treinar acompanhado pelos nossos profissionais. Além de te mostrarmos o caminho, ajudamos-te a cada passo, sempre lado a lado, focados nos teus objetivos e bem-estar.</p>
<a href="?assunto=pt#contactos" class="site-btn">Entra em contacto connosco</a>
</div>
</div>
</div>
</div>
</section>
<?php endif; /* personal trainer */ ?>
<?php
// ── Aula experimental modal ─────────────────────────────────────────────────
// Queues a booking request to id.andmore.pt (centralized mail) via site/agendar.php.
?>
<div class="modal fade trial-modal-wrap" id="trialModal" tabindex="-1" role="dialog" aria-labelledby="trialModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content trial-modal">
<div class="trial-modal-head">
<span class="section-eyebrow">FitMe 3070</span>
<h3 id="trialModalLabel">Marca a tua aula experimental</h3>
<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-bs-dismiss="modal" aria-label="Fechar">&times;</button>
</div>
<form class="trial-form" method="post" action="<?= htmlspecialchars(gym_url($gym, 'agendar')) ?>">
<?= csrf_field() ?>
<?= public_form_trap('trial') /* honeypot + carimbo + prova de JS */ ?>
<div class="row">
<div class="col-12 col-md-6 mb-3">
<input type="text" name="nome" required placeholder="Nome *" aria-label="Nome">
</div>
<div class="col-12 col-md-6 mb-3">
<input type="tel" name="telefone" required placeholder="Telemóvel *" aria-label="Telemóvel">
</div>
<div class="col-12 col-md-6 mb-3">
<input type="email" name="email" required placeholder="E-mail *" aria-label="E-mail">
</div>
<div class="col-12 col-md-6 mb-3">
<input type="date" name="data" aria-label="Dia preferido" min="<?= date('Y-m-d') ?>">
</div>
<div class="col-12 mb-3">
<select name="periodo" aria-label="Período preferido">
<option value="">Período preferido (opcional)</option>
<option value="Manhã">Manhã</option>
<option value="Tarde">Tarde</option>
<option value="Fim do dia">Fim do dia</option>
</select>
</div>
<div class="col-12 mb-3">
<textarea name="mensagem" rows="3" placeholder="Alguma modalidade ou objetivo em mente? (opcional)" aria-label="Mensagem"></textarea>
</div>
<div class="col-12">
<label class="consent-check">
<input type="checkbox" name="consent" value="1" required>
<span>Li e aceito a <a href="<?= base_url('home/privacidade') ?>" target="_blank" rel="noopener">Política de Privacidade</a>.</span>
</label>
</div>
<div class="col-12">
<button type="submit" class="site-btn w-100">Enviar pedido</button>
</div>
</div>
</form>
</div>
</div>
</div>
<?php if (($_GET['agendar'] ?? '') === 'erro'): ?>
<script>document.addEventListener('DOMContentLoaded',function(){if(window.bootstrap){new bootstrap.Modal(document.getElementById('trialModal')).show();}});</script>
<?php endif; ?>
<?php
// ── Inscrição (mensalidades) modal ──────────────────────────────────────────
// The select is pre-set by main.js from the clicked card's data-membership;
// without JS it still works as a plain chooser.
if ($memberships):
?>
<div class="modal fade trial-modal-wrap" id="joinModal" tabindex="-1" role="dialog" aria-labelledby="joinModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content trial-modal">
<div class="trial-modal-head">
<span class="section-eyebrow"><?= htmlspecialchars($info['name'] !== '' ? $info['name'] : APP_NAME) ?></span>
<h3 id="joinModalLabel">Inscreve-te</h3>
<p>Deixa os teus contactos e entramos em contacto para concluir a inscrição.</p>
<button type="button" class="trial-close" data-bs-dismiss="modal" aria-label="Fechar">&times;</button>
</div>
<form class="trial-form" method="post" action="<?= htmlspecialchars(gym_url($gym, 'inscricao')) ?>">
<?= csrf_field() ?>
<?= public_form_trap('inscricao') /* honeypot + carimbo + prova de JS */ ?>
<div class="row">
<div class="col-12 mb-3">
<select name="membership_id" aria-label="Mensalidade" required>
<?php foreach ($memberships as $m): ?>
<option value="<?= (int) $m['id'] ?>"<?= $m['is_featured'] ? ' selected' : '' ?>><?= htmlspecialchars($m['name'] . ' — ' . membership_price_label($m)) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-6 mb-3">
<input type="text" name="nome" required maxlength="150" placeholder="Nome *" aria-label="Nome">
</div>
<div class="col-12 col-md-6 mb-3">
<input type="tel" name="telefone" required maxlength="40" placeholder="Telemóvel *" aria-label="Telemóvel">
</div>
<div class="col-12 mb-3">
<input type="email" name="email" maxlength="190" placeholder="E-mail" aria-label="E-mail">
</div>
<div class="col-12 mb-3">
<textarea name="mensagem" rows="3" maxlength="1000" placeholder="Alguma dúvida ou objetivo? (opcional)" aria-label="Mensagem"></textarea>
</div>
<div class="col-12">
<label class="consent-check">
<input type="checkbox" name="consent" value="1" required>
<span>Autorizo o contacto e o tratamento dos meus dados para efeitos desta inscrição, nos termos da <a href="<?= base_url('home/privacidade') ?>" target="_blank" rel="noopener">Política de Privacidade</a>.</span>
</label>
</div>
<div class="col-12">
<button type="submit" class="site-btn w-100">Enviar pedido</button>
</div>
</div>
</form>
</div>
</div>
</div>
<?php if (($_GET['inscricao'] ?? '') === 'erro'): ?>
<script>document.addEventListener('DOMContentLoaded',function(){if(window.bootstrap){new bootstrap.Modal(document.getElementById('joinModal')).show();}});</script>
<?php endif; ?>
<?php endif; ?>
<?php
// ── Footer ────────────────────────────────────────────────────────────────────
// Newsletter hidden on request (2026-09); the partial stays in
// site/includes/newsletter.php — re-enable with: echo view('newsletter');
echo view('footer');