Compare commits
2
Commits
bab6eb87bf
...
7fa2da73dc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fa2da73dc | ||
|
|
14dbabcad5 |
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+59
-16
@@ -437,14 +437,7 @@ section[id] {
|
|||||||
|
|
||||||
.gym-switch-btn { display: inline-flex; align-items: center; gap: .5rem; }
|
.gym-switch-btn { display: inline-flex; align-items: center; gap: .5rem; }
|
||||||
|
|
||||||
/* With a logo the button shows the gym's wordmark in its own colours on a
|
|
||||||
light face, framed by the brand colour; the name is the image's alt. The
|
|
||||||
hover face stays light too, so dark parts of the logo never vanish. */
|
|
||||||
.gym-switch-btn--logo {
|
|
||||||
--btn-face: #fff;
|
|
||||||
background: #fff;
|
|
||||||
padding: 4px 12px;
|
|
||||||
}
|
|
||||||
.gym-switch-logo {
|
.gym-switch-logo {
|
||||||
display: block;
|
display: block;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
@@ -520,6 +513,34 @@ section[id] {
|
|||||||
.gym-switch-btn:is(:hover, :focus-visible)::before, .gym-switch-btn:is(:hover, :focus-visible)::after { animation: none; }
|
.gym-switch-btn:is(:hover, :focus-visible)::before, .gym-switch-btn:is(:hover, :focus-visible)::after { animation: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* With a logo the button shows the gym's wordmark in its own colours straight
|
||||||
|
on the navbar (no face, like the main logo), framed by a thin brand-colour
|
||||||
|
outline; the name is the image's alt. On hover nothing fills in behind the
|
||||||
|
logo: ::before is masked down to just its ring, so only the spinning edge
|
||||||
|
shows. */
|
||||||
|
.gym-switch-btn--logo,
|
||||||
|
.gym-switch-btn--logo:is(:hover, :focus-visible) {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.gym-switch-btn--logo {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
.gym-switch-btn--logo::before {
|
||||||
|
background: conic-gradient(from var(--pc-a), rgba(var(--fitme-accent-rgb), 0) 0deg, rgba(var(--fitme-accent-rgb), 0) 150deg,
|
||||||
|
var(--fitme-accent) 250deg, #f4ff9c 300deg, var(--fitme-accent) 330deg,
|
||||||
|
rgba(var(--fitme-accent-rgb), 0) 360deg) border-box;
|
||||||
|
-webkit-mask: linear-gradient(#000 0 0) padding-box, linear-gradient(#000 0 0);
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask: linear-gradient(#000 0 0) padding-box exclude, linear-gradient(#000 0 0);
|
||||||
|
}
|
||||||
|
/* No glow either: blurred brand colour behind a dark-red wordmark muddies it.
|
||||||
|
A thin light halo keeps the logo legible over whatever shows through the
|
||||||
|
frosted navbar. */
|
||||||
|
.gym-switch-btn--logo::after,
|
||||||
|
.gym-switch-btn--logo:is(:hover, :focus-visible)::after { display: none; }
|
||||||
|
.gym-switch-btn--logo .gym-switch-logo { filter: drop-shadow(0 0 1.5px rgba(255, 255, 255, .9)); }
|
||||||
|
|
||||||
/* ── Sticky Navbar ─────────────────────────────────────────────────────────── */
|
/* ── Sticky Navbar ─────────────────────────────────────────────────────────── */
|
||||||
.navbar-toggler {
|
.navbar-toggler {
|
||||||
min-width: 44px;
|
min-width: 44px;
|
||||||
@@ -3836,6 +3857,33 @@ h3.contact-title {
|
|||||||
background-image: var(--metal-layers);
|
background-image: var(--metal-layers);
|
||||||
background-size: var(--metal-sizes);
|
background-size: var(--metal-sizes);
|
||||||
color: var(--fitme-ash);
|
color: var(--fitme-ash);
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slow brand-colour light drifting over the metal: three soft blobs in the
|
||||||
|
current gym's colour (and a paler tint of it) on a 200% layer whose position
|
||||||
|
eases back and forth. Sits above the section background, below the content. */
|
||||||
|
.pricing-section::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
background:
|
||||||
|
radial-gradient(38% 50% at 18% 28%, rgba(var(--fitme-accent-rgb), .4), transparent 70%),
|
||||||
|
radial-gradient(34% 46% at 82% 72%, color-mix(in srgb, color-mix(in srgb, var(--fitme-accent) 55%, #fff) 26%, transparent), transparent 70%),
|
||||||
|
radial-gradient(30% 42% at 55% 105%, rgba(var(--fitme-accent-rgb), .32), transparent 70%);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
animation: pricing-drift 26s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
@keyframes pricing-drift {
|
||||||
|
0% { background-position: 0% 0%; }
|
||||||
|
50% { background-position: 100% 40%; }
|
||||||
|
100% { background-position: 30% 100%; }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.pricing-section::before { animation: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.pricing-section .section-title h2 { color: #fff; }
|
.pricing-section .section-title h2 { color: #fff; }
|
||||||
@@ -4610,14 +4658,10 @@ h3.contact-title {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── "In the middle of the screen" = hovered ─────────────────────────────────────
|
/* ── "In the middle of the screen" = hovered ─────────────────────────────────────
|
||||||
main.js adds .is-centered to the photo / card / icon that sits on the vertical
|
main.js adds .is-centered to the trainer photo / card / icon that sits on the vertical
|
||||||
middle of the viewport (and, inside the sliding strips, on the horizontal
|
middle of the viewport (and, inside the Modalidades strip, on the horizontal
|
||||||
middle too), so scrolling past shows the same state as a hover. These mirror
|
middle too; the gallery photos only react to a real hover), so scrolling past shows the same state as a hover. These mirror
|
||||||
the :hover rules above. ─────────────────────────────────────────────────── */
|
the :hover rules above. ─────────────────────────────────────────────────── */
|
||||||
.gal-tile.is-centered .gal-link img { transform: scale(1.06); filter: saturate(1) brightness(1.05); }
|
|
||||||
.gal-tile.is-centered .gal-link::before { opacity: 1; }
|
|
||||||
.gal-tile.is-centered .gal-link::after { opacity: 1; animation: pc-spin 3.2s linear infinite; }
|
|
||||||
|
|
||||||
.trainer-item .trainer-pic.is-centered,
|
.trainer-item .trainer-pic.is-centered,
|
||||||
#powerRotation>div:nth-child(even) .trainer-pic.is-centered {
|
#powerRotation>div:nth-child(even) .trainer-pic.is-centered {
|
||||||
transform: rotate(0deg) translateY(-8px) scale(1.05);
|
transform: rotate(0deg) translateY(-8px) scale(1.05);
|
||||||
@@ -4658,7 +4702,6 @@ h3.contact-title {
|
|||||||
}
|
}
|
||||||
.price-card.is-centered::after { opacity: .75; animation: pc-spin 3.2s linear infinite; }
|
.price-card.is-centered::after { opacity: .75; animation: pc-spin 3.2s linear infinite; }
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.gal-tile.is-centered .gal-link::after,
|
|
||||||
.trainer-item .trainer-pic.is-centered,
|
.trainer-item .trainer-pic.is-centered,
|
||||||
.icon-box-item.is-centered .ib-icon::after,
|
.icon-box-item.is-centered .ib-icon::after,
|
||||||
.price-card.is-centered::before, .price-card.is-centered::after { animation: none; }
|
.price-card.is-centered::before, .price-card.is-centered::after { animation: none; }
|
||||||
|
|||||||
@@ -759,16 +759,17 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/* ── Scroll "hover": what sits in the middle of the screen looks hovered ───────
|
/* ── Scroll "hover": what sits in the middle of the screen looks hovered ───────
|
||||||
Photos, cards and icons get .is-centered while their box crosses the middle
|
Trainer photos, cards and icons get .is-centered while their box crosses the
|
||||||
of the viewport (±12% of its height); inside a sliding strip (gallery,
|
middle of the viewport (±12% of its height); inside a sliding strip
|
||||||
Modalidades) they must also cross the horizontal middle. Throttled, paused
|
(Modalidades) they must also cross the horizontal middle. Gallery photos are
|
||||||
|
left out: they only light up on a real hover. Throttled, paused
|
||||||
while the tab is hidden, skipped with "reduce motion". The CSS mirrors the
|
while the tab is hidden, skipped with "reduce motion". The CSS mirrors the
|
||||||
:hover rules (style.css, "In the middle of the screen"). */
|
:hover rules (style.css, "In the middle of the screen"). */
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
if (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
if (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||||
var items = Array.prototype.slice.call(document.querySelectorAll(
|
var items = Array.prototype.slice.call(document.querySelectorAll(
|
||||||
'.gal-tile, .trainer-item .trainer-pic, .icon-box-item, .price-card')).map(function (el) {
|
'.trainer-item .trainer-pic, .icon-box-item, .price-card')).map(function (el) {
|
||||||
return { el: el, strip: !!el.closest('.gal-marquee, .mod-scroll'), on: false };
|
return { el: el, strip: !!el.closest('.mod-scroll'), on: false };
|
||||||
});
|
});
|
||||||
if (!items.length) return;
|
if (!items.length) return;
|
||||||
var last = 0;
|
var last = 0;
|
||||||
|
|||||||
@@ -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
@@ -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';
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user