release: promote beta to main (gyms, brand colours, JPEG XL, SEO per gym) #3

Merged
andre merged 74 commits from beta into main 2026-10-26 19:47:48 +00:00
5 changed files with 9 additions and 8 deletions
Showing only changes of commit 7fa2da73dc - Show all commits

No files matched your search

+1 -1
View File
@@ -79,7 +79,7 @@ Since 2026-09-25, one install serves several physical gyms: the `gyms` table, on
folder per gym (`/3070/`, `/<postal-code>/`), content rows carry `gym_id`, and the folder per gym (`/3070/`, `/<postal-code>/`), content rows carry `gym_id`, and the
backoffice has an "a editar" gym switcher (`admin_gym_id()` / backoffice has an "a editar" gym switcher (`admin_gym_id()` /
`admin_gym()` in `config/functions_gyms.php`). `/` and `/home` 301 to the default gym (`is_default`); the other published gyms appear as highlighted buttons (`.gym-switch-btn`) at the right end of the public navbar (`site/includes/navbar.php`). `site/gym_chooser.php` is no longer used. `admin_gym()` in `config/functions_gyms.php`). `/` and `/home` 301 to the default gym (`is_default`); the other published gyms appear as highlighted buttons (`.gym-switch-btn`) at the right end of the public navbar (`site/includes/navbar.php`). `site/gym_chooser.php` is no longer used.
Each gym has a brand colour (`gyms.brand_color`, `#RRGGBB`, migration `migration_20261013_gym_brand_color.sql`; gym 1 = green `#C2D500`, gym 2 = red): set in Gestão → Ginásios, emitted as `:root` overrides by `gym_brand_css()` in `site/includes/include.php`. New components must use `--fitme-accent*` / `--fitme-on-accent`, never a hard-coded colour; the CrossFit nav pill follows the brand colour too. The public navbar's bottom edge is an always-on, slow (14s) sweep in the brand colour (`.header-section::before/::after`, `nav-sweep`, static with reduced motion). Each gym can have a `logo` (same migration, plus `migration_20261026_gym_logo.sql`: the column was added to the 2026-10-13 file after it had already run on dev, so dev had no `logo` column until 2026-10-26 — never edit a migration that has run, add a new one); the header image and the logo are uploaded in Gestão → Ginásios (`gyms_save_image()` → `uploads/gyms/`, validated by content, SVG through `svg_is_safe()`), not typed as paths; `db_gyms_create/update` skip `brand_color`/`logo` while the migration isn't applied (`_gyms_cols()`); the navbar button for another gym is drawn in that gym's brand colour (`gym_brand_vars()` inline); with a logo (`gym_logo()`: the gym's own, or for the default gym the bundled `logo_black.svg`) the button shows the logo in full colour on a white face (`.gym-switch-btn--logo`, name as alt), without one it shows the name. The navbar brand on each gym's page uses that gym's logo too. Uploads (header + logo) also accept JPEG XL (`.jxl`, detected by signature in `upload_image_ext()` when the caller allows `'jxl'`; only the gyms form does; `AddType image/jxl` in `.htaccess`; GD can't resize it, so it is stored as uploaded; Chrome/Firefox may not display JXL, Safari does). Each gym has a brand colour (`gyms.brand_color`, `#RRGGBB`, migration `migration_20261013_gym_brand_color.sql`; gym 1 = green `#C2D500`, gym 2 = red): set in Gestão → Ginásios, emitted as `:root` overrides by `gym_brand_css()` in `site/includes/include.php`. New components must use `--fitme-accent*` / `--fitme-on-accent`, never a hard-coded colour; the CrossFit nav pill follows the brand colour too. The public navbar's bottom edge is an always-on, slow (14s) sweep in the brand colour (`.header-section::before/::after`, `nav-sweep`, static with reduced motion). Each gym can have a `logo` (same migration, plus `migration_20261026_gym_logo.sql`: the column was added to the 2026-10-13 file after it had already run on dev, so dev had no `logo` column until 2026-10-26 — never edit a migration that has run, add a new one); the header image and the logo are uploaded in Gestão → Ginásios (`gyms_save_image()` → `uploads/gyms/`, validated by content, SVG through `svg_is_safe()`), not typed as paths; `db_gyms_create/update` skip `brand_color`/`logo` while the migration isn't applied (`_gyms_cols()`); the navbar button for another gym is drawn in that gym's brand colour (`gym_brand_vars()` inline); with a logo (`gym_logo()`: the gym's own, or for the default gym the bundled `logo_black.svg`) the button shows the logo in full colour straight on the frosted navbar, with no face and no darkening on hover (`.gym-switch-btn--logo`: thin brand outline, hover = spinning ring only, no glow, a light `drop-shadow` halo on the image for contrast; name as alt), without one it shows the name. The navbar brand on each gym's page uses that gym's logo too. Uploads (header + logo) also accept JPEG XL (`.jxl`, detected by signature in `upload_image_ext()` when the caller allows `'jxl'`; the gyms form and gallery photos (`db_gallery_image_upload()`) do; `AddType image/jxl` in `.htaccess`; GD can't resize it, so it is stored as uploaded; Chrome/Firefox may not display JXL, Safari does). The public gallery lists its folder by extension (`site/includes/gallery.php`, `site/index.php`: jpg/jpeg/png/gif/webp/jxl; before 2026-10-26 it only listed `*.jpg`, so PNG/WebP uploads never showed). Gallery photos are not in the scroll "hover" (`.is-centered`, main.js) any more; only a real hover lights them. Mensalidades (`.pricing-section::before`) has a slow (26s) drifting brand-colour glow over the metal texture (`pricing-drift`, static with reduced motion).
An unpublished gym 404s publicly but previews for staff. An unpublished gym 404s publicly but previews for staff.
## platform-tools ## platform-tools
+2 -2
View File
@@ -55,7 +55,7 @@ function db_gallery_image_upload(PDO $pdo, int $gallery_id, array $file): ?strin
if (($file['size'] ?? 0) > 16 * 1024 * 1024) return null; if (($file['size'] ?? 0) > 16 * 1024 * 1024) return null;
// Tipo decidido pelo conteúdo, não pelo nome (config/functions_images.php). // Tipo decidido pelo conteúdo, não pelo nome (config/functions_images.php).
$ext = upload_image_ext($file, ['jpg', 'jpeg', 'png', 'gif', 'webp']); $ext = upload_image_ext($file, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'jxl']);
if ($ext === null) return null; if ($ext === null) return null;
$upload_dir = __DIR__ . '/../uploads/gallery/' . $gallery_id . '/'; $upload_dir = __DIR__ . '/../uploads/gallery/' . $gallery_id . '/';
@@ -66,7 +66,7 @@ function db_gallery_image_upload(PDO $pdo, int $gallery_id, array $file): ?strin
img_optimize($upload_dir . $filename, IMG_MAX_GALLERY); img_optimize($upload_dir . $filename, IMG_MAX_GALLERY);
$stmt = $pdo->prepare("INSERT INTO gallery (i_file_name, i_file_type, i_project_id) VALUES (?, ?, ?)"); $stmt = $pdo->prepare("INSERT INTO gallery (i_file_name, i_file_type, i_project_id) VALUES (?, ?, ?)");
$stmt->execute([$filename, $file['type'] ?? 'image/jpeg', $gallery_id]); $stmt->execute([$filename, $ext === 'jxl' ? 'image/jxl' : ($file['type'] ?? 'image/jpeg'), $gallery_id]);
return $filename; return $filename;
} }
+3 -2
View File
@@ -6,8 +6,9 @@
// tile (without dragging) opens it full size in a <dialog> lightbox with prev / // tile (without dragging) opens it full size in a <dialog> lightbox with prev /
// next, arrow keys, Esc and swipe; the repeated copies are hidden from assistive // next, arrow keys, Esc and swipe; the repeated copies are hidden from assistive
// tech and open the same photo. // tech and open the same photo.
$files = glob(rtrim($gallery_path, '/') . '/*.jpg'); // Every format the admin upload accepts (functions_galerias.php), not just .jpg.
if (!$files || !is_array($files)) $files = []; $files = preg_grep('/\.(jpe?g|png|gif|webp|jxl)$/i', glob(rtrim($gallery_path, '/') . '/*') ?: []);
$files = array_values($files ?: []);
// Column span per tile, cycled: w = two columns, s = one. // Column span per tile, cycled: w = two columns, s = one.
$gal_shapes = ['w', 's', 's', 's', 's', 'w', 's', 's', 's', 's', 's']; $gal_shapes = ['w', 's', 's', 's', 's', 'w', 's', 's', 's', 's', 's'];
$gal_label = $gallery_h2 ?? 'Instalações'; $gal_label = $gallery_h2 ?? 'Instalações';
+1 -1
View File
@@ -117,7 +117,7 @@ if ($offer):
<?php <?php
// ── Galeria (Instalações) ─────────────────────────────────────────────────────── // ── Galeria (Instalações) ───────────────────────────────────────────────────────
$gallery_path = !empty($gym['gallery_id']) ? 'uploads/gallery/' . (int) $gym['gallery_id'] . '/' : ''; $gallery_path = !empty($gym['gallery_id']) ? 'uploads/gallery/' . (int) $gym['gallery_id'] . '/' : '';
$gallery_files = ($on('galeria') && $gallery_path !== '') ? glob($gallery_path . '*.jpg') : []; $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): if ($gallery_files && count($gallery_files) > 0):
$gallery_h2 = 'Conhece as nossas instalações'; $gallery_h2 = 'Conhece as nossas instalações';
?> ?>
+2 -2
View File
@@ -82,8 +82,8 @@ include __DIR__ . '/../../../includes/header.php';
<form method="POST" enctype="multipart/form-data"> <form method="POST" enctype="multipart/form-data">
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="mb-3"> <div class="mb-3">
<input type="file" class="form-control" name="image" accept=".jpg,.jpeg,.png,.gif,.webp"> <input type="file" class="form-control" name="image" accept=".jpg,.jpeg,.png,.gif,.webp,.jxl,image/jxl">
<div class="form-text">JPG, PNG, GIF ou WebP.</div> <div class="form-text">JPG, PNG, GIF, WebP ou JPEG XL (o JPEG XL não é redimensionado e só aparece em browsers que o suportam, como o Safari).</div>
</div> </div>
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-cloud-arrow-up me-1"></i>Carregar</button> <button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-cloud-arrow-up me-1"></i>Carregar</button>
</form> </form>