- Ícones novos (Bootcamp, YourFit Core, Run3070, Total Workout, On Ramp, Jump) do mesmo conjunto; só o LPO continua sem ícone no ficheiro de design. - Os ícones fitme-*.svg passam a ir embutidos (modalidade_icon_html): cada traço tem pathLength=1 e desenha-se ao aparecer, no hover e ao abrir o cartão; cada família tem um movimento (levantar, soco, respirar, saltar, chama, pedalar, correr). Outros ícones continuam <img>. - No cartão de detalhe (desktop) o ícone e o nome deslizam até ao centro vertical da coluna. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
179 lines
8.6 KiB
PHP
179 lines
8.6 KiB
PHP
<?php
|
|
// ─── Public-site data (plain-PDO replacements for the old CI4 models) ─────────
|
|
// The ported website/* partials read the same shapes the CI4 models returned.
|
|
// Every query is defensive — the gym-content tables can be empty on a fresh DB.
|
|
|
|
// fitme3070_info row (id=1). Always returns every key the views read, so empty
|
|
// installs render without warnings.
|
|
function site_info(PDO $pdo): array {
|
|
$cols = ['id','name','adress','phone','telephone','whatsapp','instagram',
|
|
'facebook','messenger','email','website','google_maps','google_maps_link'];
|
|
$row = [];
|
|
try {
|
|
$row = $pdo->query("SELECT * FROM fitme3070_info WHERE id = 1 LIMIT 1")
|
|
->fetch(PDO::FETCH_ASSOC) ?: [];
|
|
} catch (Throwable $e) { $row = []; }
|
|
foreach ($cols as $c) {
|
|
if (!array_key_exists($c, $row) || $row[$c] === null) $row[$c] = '';
|
|
}
|
|
// wa.me only accepts digits — a stray "+" or space would break the link.
|
|
$row['whatsapp'] = preg_replace('/\D+/', '', (string) $row['whatsapp']);
|
|
return $row;
|
|
}
|
|
|
|
// Top navigation map (label => CI-style path). Kept identical to the original
|
|
// so navbar/footer markup is unchanged; .htaccess rewrites these to site/*.php.
|
|
function site_navbar(): array {
|
|
// One-page layout: Home, Equipa, Modalidades, Horário, Contactos are all
|
|
// anchor links on the single homepage. CrossFit is a separate page styled
|
|
// differently (see navbar.php for the 'crossfit_btn' flag).
|
|
return [
|
|
'Home' => '#home',
|
|
'Equipa' => '#equipa',
|
|
'Modalidades' => '#modalidades',
|
|
'Horário' => '#horario',
|
|
'Mensalidades' => '#mensalidades',
|
|
'Contactos' => '#contactos',
|
|
'CrossFit' => 'home/crossfit', // separate page, navbar styles it specially
|
|
];
|
|
}
|
|
|
|
function site_offer(PDO $pdo): array {
|
|
try {
|
|
return $pdo->query(
|
|
"SELECT * FROM website_offer
|
|
WHERE o_deleted_at IS NULL OR o_deleted_at = '0000-00-00 00:00:00' OR o_deleted_at = ''"
|
|
)->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (Throwable $e) { return []; }
|
|
}
|
|
|
|
function site_work_hours(PDO $pdo): array {
|
|
try { return $pdo->query("SELECT * FROM work_hours")->fetchAll(PDO::FETCH_ASSOC); }
|
|
catch (Throwable $e) { return []; }
|
|
}
|
|
|
|
function site_team(PDO $pdo): array {
|
|
try { return $pdo->query("SELECT * FROM team WHERE deleted_at IS NULL")->fetchAll(PDO::FETCH_ASSOC); }
|
|
catch (Throwable $e) { return []; }
|
|
}
|
|
|
|
function site_modalidades(PDO $pdo): array {
|
|
try {
|
|
return $pdo->query(
|
|
"SELECT * FROM modalidades WHERE deleted_at IS NULL ORDER BY item_order DESC"
|
|
)->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (Throwable $e) { return []; }
|
|
}
|
|
|
|
// Currently-visible time-limited events for the homepage banner. Thin wrapper
|
|
// over db_events_active() (functions_events.php) so the site code reads in the
|
|
// same site_*() style as everything else here.
|
|
function site_active_events(PDO $pdo): array {
|
|
return function_exists('db_events_active') ? db_events_active($pdo) : [];
|
|
}
|
|
|
|
// ─── Image-path resolver ──────────────────────────────────────────────────────
|
|
// DB stores either a bare filename ("1.jpg") or a full relative path
|
|
// ("uploads/team/foto/1.jpg"). This normalises both to a working URL.
|
|
// $base_dir is the upload subdirectory (e.g. "uploads/team/foto").
|
|
function site_img_url($value, $base_dir): string {
|
|
// Coerce null/empty to empty string
|
|
$value = (string) $value;
|
|
if ($value === '') return '';
|
|
// Already a full URL
|
|
if (substr($value, 0, 4) === 'http') return $value;
|
|
// Already has the uploads/ prefix — use as-is
|
|
if (substr($value, 0, 8) === 'uploads/') return base_url($value);
|
|
// Bare filename — prepend the base directory
|
|
return base_url(rtrim((string) $base_dir, '/') . '/' . ltrim($value, '/'));
|
|
}
|
|
|
|
// Schedule joined to modalidades (name/colour/icon), optionally filtered by
|
|
// local (e.g. 'crossfit'). Mirrors Horario_model::getAll().
|
|
function site_horario(PDO $pdo, string $local = ''): array {
|
|
$sql = "SELECT h.*,
|
|
TIME_FORMAT(h.hora_inicio, '%H:%i') AS hora_inicio,
|
|
TIME_FORMAT(h.hora_fim, '%H:%i') AS hora_fim,
|
|
m.nome, m.cor, m.icon
|
|
FROM horario h
|
|
JOIN modalidades m ON m.m_id = h.id_modalidade";
|
|
$args = [];
|
|
if ($local !== '') { $sql .= " WHERE h.local = ?"; $args[] = $local; }
|
|
$sql .= " ORDER BY h.hora_inicio ASC, m.nome ASC";
|
|
try { $st = $pdo->prepare($sql); $st->execute($args); return $st->fetchAll(PDO::FETCH_ASSOC); }
|
|
catch (Throwable $e) { return []; }
|
|
}
|
|
|
|
// ─── Indexing ─────────────────────────────────────────────────────────────────
|
|
// SEO_NOINDEX=true in .env keeps an install out of search results (meta robots
|
|
// + robots.txt "Disallow: /"). For staging copies such as fitme.andmore.pt,
|
|
// which serve the same content as the real domain and would otherwise compete
|
|
// with it as duplicate content.
|
|
function seo_noindex(): bool {
|
|
return filter_var(env('SEO_NOINDEX', 'false'), FILTER_VALIDATE_BOOLEAN);
|
|
}
|
|
|
|
// Public, indexable URLs for sitemap.xml: the fixed pages plus any live event
|
|
// landing page.
|
|
function site_sitemap_urls(PDO $pdo): array {
|
|
$urls = [
|
|
['loc' => base_url(), 'priority' => '1.0'],
|
|
['loc' => base_url('home/crossfit'), 'priority' => '0.8'],
|
|
['loc' => base_url('home/privacidade'), 'priority' => '0.2'],
|
|
];
|
|
foreach (site_active_events($pdo) as $ev) {
|
|
$urls[] = ['loc' => base_url('evento/' . rawurlencode($ev['slug'])), 'priority' => '0.6'];
|
|
}
|
|
return $urls;
|
|
}
|
|
|
|
// E.164 form for tel: links and structured data. Numbers are stored however
|
|
// the admin typed them ("964 201 137", "+351 231 451 346"); a bare 9-digit
|
|
// Portuguese number gets +351 so the link also works from a foreign phone.
|
|
function site_tel(?string $phone): string {
|
|
$p = preg_replace('/[^\d+]/', '', (string) $phone);
|
|
if ($p === '') return '';
|
|
if ($p[0] !== '+' && strlen($p) === 9) return '+351' . $p;
|
|
if ($p[0] !== '+' && str_starts_with($p, '00')) return '+' . substr($p, 2);
|
|
return $p;
|
|
}
|
|
|
|
// ─── Modalidade icon markup ───────────────────────────────────────────────────
|
|
// The FitMe icon set (uploads/modalidades/icons/fitme-*.svg, shipped in the repo
|
|
// — backoffice uploads are always named upl_*) is inlined so CSS can animate
|
|
// the strokes: every drawable element gets pathLength="1" for the draw-in, and
|
|
// the <svg> carries data-anim, a per-family movement (see style.css). Anything
|
|
// else (legacy PNGs, uploaded SVGs) stays a plain <img>.
|
|
function modalidade_icon_anim(string $slug): string {
|
|
$map = [
|
|
'lift' => ['yourfit-pump', 'crossfit', 'crossfit-teen-s', 'fbb', 'yourfit-core', 'total-workout'],
|
|
'punch' => ['yourfit-fight', 'bootcamp'],
|
|
'breathe' => ['yourfit-mind', 'yoga', 'pilates'],
|
|
'bounce' => ['zumba', 'jump', 'yourfit-pulse', 'hybrid'],
|
|
'flicker' => ['fitburn'],
|
|
'ride' => ['cycling'],
|
|
'run' => ['run3070', 'on-ramp', 'localizada'],
|
|
];
|
|
foreach ($map as $anim => $slugs) if (in_array($slug, $slugs, true)) return $anim;
|
|
return 'lift';
|
|
}
|
|
|
|
function modalidade_icon_html(?string $icon): string {
|
|
$icon = trim((string) $icon);
|
|
if ($icon === '') return '';
|
|
if (preg_match('/^fitme-([a-z0-9-]+)\.svg$/', $icon, $m)) {
|
|
$file = dirname(__DIR__) . '/uploads/modalidades/icons/' . $icon;
|
|
$svg = is_file($file) ? (string) file_get_contents($file) : '';
|
|
if (str_starts_with(ltrim($svg), '<svg')) {
|
|
// Belt and braces: no scripts / event handlers, whatever the file says.
|
|
$svg = preg_replace('~<script\b.*?</script>~is', '', $svg);
|
|
$svg = preg_replace('~\son\w+\s*=\s*("[^"]*"|\'[^\']*\')~i', '', $svg);
|
|
$svg = preg_replace('~\scolor="[^"]*"~', '', $svg, 1); // colour comes from CSS
|
|
$svg = preg_replace('~<(path|circle|rect|line|polyline|polygon|ellipse)\b~', '<$1 pathLength="1"', $svg);
|
|
$svg = preg_replace('~^\s*<svg\b~', '<svg class="mi" data-anim="' . modalidade_icon_anim($m[1]) . '" aria-hidden="true" focusable="false"', $svg, 1);
|
|
return $svg;
|
|
}
|
|
}
|
|
return '<img loading="lazy" draggable="false" src="' . htmlspecialchars(site_img_url($icon, 'uploads/modalidades/icons')) . '" alt="">';
|
|
}
|