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:
1 parent
2f8f68c61a
commit
5e300fc341
33 files changed
+1146
-197
No files matched your search
+70
-52
@@ -3,15 +3,23 @@
|
||||
// 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 {
|
||||
// Every site_*() reader takes the gym (functions_gyms.php); by default the one
|
||||
// the current page renders (current_gym()), so single-gym code paths — the
|
||||
// privacy page, events, mail handlers — keep working unchanged.
|
||||
function _site_gym_id(?int $gym_id): int {
|
||||
return $gym_id ?? (int) current_gym()['id'];
|
||||
}
|
||||
|
||||
// fitme3070_info row of the gym (id = gyms.id). Always returns every key the
|
||||
// views read, so empty installs render without warnings.
|
||||
function site_info(PDO $pdo, ?int $gym_id = null): 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) ?: [];
|
||||
$st = $pdo->prepare("SELECT * FROM fitme3070_info WHERE id = ? LIMIT 1");
|
||||
$st->execute([_site_gym_id($gym_id)]);
|
||||
$row = $st->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
} catch (Throwable $e) { $row = []; }
|
||||
foreach ($cols as $c) {
|
||||
if (!array_key_exists($c, $row) || $row[$c] === null) $row[$c] = '';
|
||||
@@ -21,55 +29,63 @@ function site_info(PDO $pdo): array {
|
||||
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
|
||||
];
|
||||
// Top navigation (label => absolute URL): anchors on the gym's own page, only
|
||||
// for the sections that gym shows, plus its CrossFit page when it has one.
|
||||
function site_navbar(?array $gym = null): array {
|
||||
$gym = $gym ?? current_gym();
|
||||
$items = ['Home' => ['#home', null]];
|
||||
foreach (['Modalidades' => 'modalidades', 'Equipa' => 'equipa', 'Horário' => 'horario',
|
||||
'Mensalidades' => 'mensalidades', 'Contactos' => 'contactos'] as $label => $sec) {
|
||||
if (gym_section_on($gym, $sec)) $items[$label] = ['#' . $sec, $sec];
|
||||
}
|
||||
$nav = [];
|
||||
foreach ($items as $label => [$hash]) $nav[$label] = gym_url($gym, $hash);
|
||||
if (!empty($gym['has_crossfit'])) $nav['CrossFit'] = gym_url($gym, 'crossfit');
|
||||
return $nav;
|
||||
}
|
||||
|
||||
function site_offer(PDO $pdo): array {
|
||||
function site_offer(PDO $pdo, ?int $gym_id = null): array {
|
||||
try {
|
||||
return $pdo->query(
|
||||
$st = $pdo->prepare(
|
||||
"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);
|
||||
WHERE gym_id = ? AND (o_deleted_at IS NULL OR o_deleted_at = '0000-00-00 00:00:00' OR o_deleted_at = '')"
|
||||
);
|
||||
$st->execute([_site_gym_id($gym_id)]);
|
||||
return $st->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable $e) { return []; }
|
||||
}
|
||||
|
||||
function site_work_hours(PDO $pdo): array {
|
||||
try { return $pdo->query("SELECT * FROM work_hours WHERE deleted_at IS NULL")->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 {
|
||||
function site_work_hours(PDO $pdo, ?int $gym_id = null): array {
|
||||
try {
|
||||
return $pdo->query(
|
||||
"SELECT * FROM modalidades WHERE deleted_at IS NULL ORDER BY item_order DESC"
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
$st = $pdo->prepare("SELECT * FROM work_hours WHERE deleted_at IS NULL AND gym_id = ?");
|
||||
$st->execute([_site_gym_id($gym_id)]);
|
||||
return $st->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) : [];
|
||||
function site_team(PDO $pdo, ?int $gym_id = null): array {
|
||||
try {
|
||||
$st = $pdo->prepare("SELECT * FROM team WHERE deleted_at IS NULL AND gym_id = ?");
|
||||
$st->execute([_site_gym_id($gym_id)]);
|
||||
return $st->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable $e) { return []; }
|
||||
}
|
||||
|
||||
function site_modalidades(PDO $pdo, ?int $gym_id = null): array {
|
||||
try {
|
||||
$st = $pdo->prepare("SELECT * FROM modalidades WHERE deleted_at IS NULL AND gym_id = ? ORDER BY item_order DESC");
|
||||
$st->execute([_site_gym_id($gym_id)]);
|
||||
return $st->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable $e) { return []; }
|
||||
}
|
||||
|
||||
// Currently-visible time-limited events for the homepage banner: the gym's own
|
||||
// plus those for every gym (gym_id NULL).
|
||||
function site_active_events(PDO $pdo, ?int $gym_id = null): array {
|
||||
if (!function_exists('db_events_active')) return [];
|
||||
$gid = _site_gym_id($gym_id);
|
||||
return array_values(array_filter(db_events_active($pdo), fn($e) =>
|
||||
!isset($e['gym_id']) || $e['gym_id'] === null || (int) $e['gym_id'] === $gid));
|
||||
}
|
||||
|
||||
// ─── Image-path resolver ──────────────────────────────────────────────────────
|
||||
@@ -90,15 +106,15 @@ function site_img_url($value, $base_dir): string {
|
||||
|
||||
// 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 {
|
||||
function site_horario(PDO $pdo, string $local = '', ?int $gym_id = null): 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 AND m.deleted_at IS NULL
|
||||
WHERE h.deleted_at IS NULL";
|
||||
$args = [];
|
||||
WHERE h.deleted_at IS NULL AND m.gym_id = ?";
|
||||
$args = [_site_gym_id($gym_id)];
|
||||
if ($local !== '') { $sql .= " AND 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); }
|
||||
@@ -117,12 +133,14 @@ function seo_noindex(): bool {
|
||||
// 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) {
|
||||
// One entry per PUBLISHED gym (unpublished ones must not be discoverable).
|
||||
$urls = [];
|
||||
foreach (db_gyms_published($pdo) ?: [gym_fallback()] as $g) {
|
||||
$urls[] = ['loc' => gym_url($g), 'priority' => '1.0'];
|
||||
if (!empty($g['has_crossfit'])) $urls[] = ['loc' => gym_url($g, 'crossfit'), 'priority' => '0.8'];
|
||||
}
|
||||
$urls[] = ['loc' => base_url('home/privacidade'), 'priority' => '0.2'];
|
||||
foreach (function_exists('db_events_active') ? db_events_active($pdo) : [] as $ev) {
|
||||
$urls[] = ['loc' => base_url('evento/' . rawurlencode($ev['slug'])), 'priority' => '0.6'];
|
||||
}
|
||||
return $urls;
|
||||
|
||||
Reference in new issue
Block a user