Files
fitme3070.pt/site/index.php
T
andreandClaude Sonnet 5 5cf0f14ad2 fix: correções Baixo da auditoria (escaping, dvh, transições, assets)
Fecha os itens 3.2.2, 3.3.1, 4.6, 5.2, 5.6, 5.7 e 7.4 do relatório de
auditoria de 2026-09-24: escapa saídas de site_img_url()/canonical,
valida identificadores de coluna em db_info_save(), troca 100vh por
100dvh, remove a última cor solta, encurta a transição para ~150ms,
acrescenta decoding=async/fetchpriority=high nas imagens da home, e
documenta as versões vendorizadas em ASSETS.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 12:16:33 +01:00

863 lines
50 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 gym chooser.
if (($_GET['gym'] ?? '') === '') {
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../config/functions.php';
$published = db_gyms_published($pdo);
if (count($published) > 1) { require __DIR__ . '/gym_chooser.php'; exit; }
$to = $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) : [];
$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="site-btn sb-ghost" data-toggle="modal" data-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" 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 !== '') ? glob($gallery_path . '*.jpg') : [];
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_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" 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" 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 mb-2">
<button type="button" class="schedule-dl-btn" id="scheduleDownload">
<i class="fas fa-download" aria-hidden="true"></i> Guardar horário (JPG)
</button>
</div>
</div>
</div>
<!-- Day picker: click a day to view just its classes -->
<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>
<?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;">
<div class="rounded_frame"><?= $key ?></div>
</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']) ?>" 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>
</div>
</div>
<p class="schedule-scroll-hint">&larr; Deslize para ver todos os dias &rarr;</p>
</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 f = board.querySelector('.schedule-day-filter'); if (f) f.parentNode.removeChild(f);
var b = doc.getElementById('scheduleDownload'); if (b) { var w = b.closest('div'); (w || b).parentNode.removeChild(w || b); }
// Un-hide every day (mobile day-filter 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-toggle="modal" data-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="site-btn sb-line" 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;
}
$session = session();
?>
<?php if ($on('contactos')): ?>
<?php /* contacts-bg.jpg never existed — outside2.jpg is the current photo in img/contactos/ */ ?>
<section id="contactos" class="contact-section set-bg" data-setbg="<?= base_url('site/assets/img/contactos/outside2.jpg') ?>" aria-labelledby="contactos-title">
<div class="container">
<?php if (($_GET['agendar'] ?? '') === 'ok'): ?>
<div class="alert alert-success text-center" role="alert">
Pedido enviado! Entraremos em contacto para confirmar a tua aula experimental.
</div>
<?php elseif (($_GET['agendar'] ?? '') === '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="row">
<div class="col-lg-4">
<h2 id="contactos-title" class="contact-title text-center text-md-left">Informações</h2>
<div class="contact-info-warp text-center text-md-left">
<h3>Localização</h3>
<div class="contact-info text-center text-md-left">
<i class="fas fa-map-marker-alt"></i>
<div class="cf-text">
<a href="<?= htmlspecialchars($info['google_maps_link'] ?? '') ?>" target="_blank" rel="noopener">
<p><?= htmlspecialchars($info['adress'] ?? '') ?></p>
</a>
</div>
</div>
</div>
<div class="contact-info-warp text-center text-md-left">
<h3>Telefone</h3>
<div class="contact-info text-center text-md-left">
<i class="fas fa-phone-volume"></i>
<div class="cf-text">
<a href="tel:<?= htmlspecialchars(site_tel($info['phone'] ?? '')) ?>">
<p><?= htmlspecialchars($info['phone'] ?? '') ?></p>
</a>
<?php if (($info['telephone'] ?? '') !== ''): ?>
<a href="tel:<?= htmlspecialchars(site_tel($info['telephone'])) ?>">
<p><?= htmlspecialchars($info['telephone']) ?></p>
</a>
<?php endif; ?>
</div>
</div>
</div>
<div class="contact-info-warp text-center text-md-left">
<h3>E-mail</h3>
<div class="contact-info">
<i class="fas fa-envelope"></i>
<div class="cf-text">
<a href="mailto:<?= htmlspecialchars($info['email'] ?? '') ?>">
<p><?= htmlspecialchars($info['email'] ?? '') ?></p>
</a>
<a href="#contactos">
<p><?= htmlspecialchars($info['website'] ?? '') ?></p>
</a>
</div>
</div>
</div>
<?php // Mapa compacto, ao lado das informações ?>
<div class="map map-side" id="map">
<?php
// Defer the Google Maps iframe until the user scrolls near it.
$maps_embed = $info['google_maps'] ?? '';
if (strpos($maps_embed, 'loading=') === false) {
$maps_embed = str_replace('<iframe ', '<iframe loading="lazy" ', $maps_embed);
}
echo map_click_to_load(clean_map_embed($maps_embed), (string) ($info['google_maps_link'] ?? ''));
?>
</div>
</div>
<div class="col-lg-8">
<div id="contact-form">
<h3 class="contact-title text-center text-md-left">Envia-nos uma mensagem</h3>
<?php if (isset($_GET['enviado'])): ?>
<div class="alert alert-success" role="alert">
Mensagem enviada com sucesso! Entraremos em contacto brevemente.
</div>
<?php endif; ?>
<form class="contact-form" method="post" action="https://forms.zohopublic.eu/fitme30701/form/FormulriodeContacto/formperma/NiwsTjwWGX2sgpZIXqE5otyAn8D5nuRQjpYDuGkS2wM/htmlRecords/submit">
<input type="hidden" name="zf_redirect_url" value="<?= base_url('?enviado=1#contact-form') ?>">
<div class="row">
<div class="col-md-6">
<input type="text" required name="SingleLine" placeholder="Nome" aria-label="Nome">
</div>
<div class="col-md-6">
<input type="text" required name="PhoneNumber_countrycode" placeholder="Telemóvel" aria-label="Telemóvel">
</div>
</div>
<input type="text" name="SingleLine1" placeholder="Razão" required value="<?= htmlspecialchars($assunto) ?>" aria-label="Razão do contacto">
<textarea name="MultiLine" placeholder="Mensagem" required aria-label="Mensagem"></textarea>
<?php if ($session->getFlashdata('email') !== null): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?= $session->getFlashdata('email') ?>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<?php endif; ?>
<label class="consent-check">
<?php /* no name attribute: gates submission client-side, not stored in Zoho */ ?>
<input type="checkbox" required>
<span>Li e aceito a <a href="<?= base_url('home/privacidade') ?>" target="_blank" rel="noopener">Política de Privacidade</a>.</span>
</label>
<button class="site-btn" id="btn_email" type="submit">Enviar Mensagem</button>
</form>
</div>
</div>
</div>
</div>
</section>
<?php endif; /* contactos */ ?>
<?php
// ── Horário de Funcionamento ──────────────────────────────────────────────────
if ($whours):
$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
?>
<section id="workhours" class="workhours-section" aria-labelledby="workhours-title">
<div class="container">
<div class="section-title text-center deco">
<span class="section-eyebrow">Visita-nos</span>
<h2 id="workhours-title">Horário de Funcionamento</h2>
</div>
<?php // Compact day / hours cards. A <dl> so the pairs read as data
// (label → value) for screen readers and search engines. ?>
<dl class="wh-grid">
<?php foreach ($work_hours_days as $key => $value): ?>
<div class="wh-day">
<dt><?= htmlspecialchars($key) ?></dt>
<?php $any = false; foreach ($whours as $h): if ($h['week_day'] == $value): $any = true; ?>
<dd><?= htmlspecialchars(substr($h['open'], 0, 5) . ' – ' . substr($h['close'], 0, 5)) ?></dd>
<?php endif; endforeach; ?>
<?php if (!$any): ?><dd class="wh-closed">Encerrado</dd><?php endif; ?>
</div>
<?php endforeach; ?>
</dl>
</div>
</section>
<?php endif; ?>
<?php
// ── 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-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="form-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.jQuery){jQuery('#trialModal').modal('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-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="form-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.jQuery){jQuery('#joinModal').modal('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');