feat: vários ginásios (/3070/ e /<código postal>/), CRUD de ginásios, modalidades após a galeria

Estrutura multi-ginásio: as mesmas secções do site, conteúdo próprio por
ginásio, e cada um numa pasta da URL com o código postal da zona.

- migration_20260926_gyms: tabela gyms (pasta, nome, localidade, publicado,
  principal, secções visíveis, título/imagem do header, SEO, galeria, Place ID,
  página CrossFit) com o FitMe 3070 como ginásio 1 em /3070/; gym_id em team,
  modalidades (o horário segue a modalidade), work_hours, memberships,
  membership_leads e website_offer (tudo o existente → ginásio 1); events com
  gym_id NULL = todos; fitme3070_info passa a uma linha por ginásio (id = gym).
- Site: /<pasta>/, /<pasta>/crossfit, /<pasta>/agendar|inscricao. Ginásio por
  publicar → 404 para o público, pré-visualização (com faixa) para a equipa.
  / e /home → 301 para o ginásio principal (a manter a query); com mais de um
  ginásio publicado, a raiz passa a uma página para escolher. Secções
  desligadas não aparecem nem no menu; menu, canonical, JSON-LD, sitemap,
  formulários, e-mails e reviews (Place ID e cache) são do ginásio.
- Backoffice: Gestão → Ginásios (criar/editar/apagar, secções, e «começar
  com o conteúdo de» outro ginásio — cópias independentes) e o seletor
  «A editar» no topo; equipa, modalidades, horários, horário de
  funcionamento, mensalidades, pedidos, contactos e reviews trabalham sobre o
  ginásio escolhido, e um registo de outro ginásio não abre.
- Modalidades passam para logo a seguir à galeria das instalações.
- Galeria sem cantos arredondados (raio da marca).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
andreandClaude Opus 5.5 committed 2026-09-25 07:26:40 +01:00
1 parent 2f8f68c61a
commit 5e300fc341
33 files changed
+1146 -197

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
<?php
// Root page when more than one gym is published (site/index.php includes this
// instead of redirecting): pick a gym. Self-contained — no gym is "current"
// here, so it doesn't go through _open.php. Expects $pdo and $published.
$gyms = $published ?? db_gyms_published($pdo);
$brand = 'FitMe';
?><!doctype html>
<html lang="pt">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($brand) ?> — Escolhe o teu ginásio</title>
<meta name="description" content="<?= htmlspecialchars($brand) ?>: <?= htmlspecialchars(implode(', ', array_map(fn($g) => $g['name'] . ($g['locality'] !== '' ? ' (' . $g['locality'] . ')' : ''), $gyms))) ?>.">
<link rel="canonical" href="<?= htmlspecialchars(base_url()) ?>">
<?php if (seo_noindex()): ?><meta name="robots" content="noindex, nofollow"><?php endif; ?>
<link href="https://fonts.googleapis.com/css?family=Roboto:400,700,900&display=swap" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="<?= base_url('site/assets/img/logos/logo_black.svg') ?>">
<style>
:root { --accent:#C2D500; --dark:#0B0B0D; --dark2:#16181A; --ash:#B8B9BE; }
* { box-sizing: border-box; }
body { margin:0; min-height:100svh; font-family:Roboto,system-ui,sans-serif; color:#fff;
background:#141517 url('<?= base_url('site/assets/img/home.jpg') ?>') center/cover fixed; }
.wrap { min-height:100svh; background:rgba(11,11,13,.82); display:flex; flex-direction:column;
align-items:center; justify-content:center; padding:48px 16px; gap:36px; }
h1 { margin:0; font-size:clamp(1.8rem,4vw,2.6rem); font-weight:900; text-transform:uppercase; text-align:center; }
h1 span { color:var(--accent); }
.gyms { display:flex; flex-wrap:wrap; gap:18px; justify-content:center; width:100%; max-width:900px; }
a.gym { flex:1 1 260px; max-width:320px; display:block; padding:28px 24px; border-radius:8px; text-decoration:none;
color:#fff; background:var(--dark2); border:2px solid rgba(255,255,255,.08);
transition:border-color .25s, transform .25s; }
a.gym:hover, a.gym:focus-visible { border-color:var(--accent); transform:translateY(-4px); outline:none; }
.gym strong { display:block; font-size:1.4rem; font-weight:900; text-transform:uppercase; }
.gym span { color:var(--ash); }
.gym em { display:inline-block; margin-top:14px; font-style:normal; font-weight:700; color:var(--accent); }
</style>
</head>
<body>
<main class="wrap">
<img src="<?= base_url('site/assets/img/logos/logo_black.svg') ?>" alt="<?= htmlspecialchars($brand) ?>" style="width:170px;filter:invert(1)">
<h1>Escolhe o teu <span>ginásio</span></h1>
<div class="gyms">
<?php foreach ($gyms as $g): ?>
<a class="gym" href="<?= htmlspecialchars(gym_url($g)) ?>">
<strong><?= htmlspecialchars($g['name']) ?></strong>
<?php if ($g['locality'] !== ''): ?><span><?= htmlspecialchars($g['locality']) ?></span><?php endif; ?>
<br><em>Entrar →</em>
</a>
<?php endforeach; ?>
</div>
</main>
</body>
</html>