From 8ebf20f52f361d35fca677faacc2a3f6b0e898c7 Mon Sep 17 00:00:00 2001 From: andre Date: Thu, 24 Sep 2026 21:46:21 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20hor=C3=A1rio=20em=203=20dias,=20hor?= =?UTF-8?q?=C3=A1rio=20de=20funcionamento=20compacto,=20textura=20na=20equ?= =?UTF-8?q?ipa,=20estrutura/SEO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Horário de aulas: cartões mais pequenos e estreitos; a partir de 992px mostra o dia anterior, hoje ao centro e o seguinte (a dar a volta à semana), com as horas alinhadas e os dias laterais esbatidos. O JPG continua a semana toda. - Horário de funcionamento: cartões compactos numa
, ordenados por hora. - Equipa: fundo no cinzento da parede do ginásio com juntas de blocos e grão muito discretos (SVG inline). - Tira o «FitMe 3070» do header e esconde a newsletter. - Estrutura: um
em todas as páginas públicas (skip link aponta-lhe), h2 por secção com aria-labelledby, h3 nos itens em vez de h4, sem secções aninhadas, alts distintos na galeria e no h1 do CrossFit. - robots.txt e sitemap.xml gerados com o domínio da instalação; SEO_NOINDEX no .env tira uma cópia de staging dos motores de pesquisa. Co-Authored-By: Claude Opus 5.5 --- .htaccess | 2 + config/functions_site.php | 23 +++++ site/_open.php | 3 + site/assets/css/style.css | 166 ++++++++++++++++++++++++++++++++- site/assets/js/main.js | 47 ++++++++-- site/includes/footer.php | 2 + site/includes/gallery.php | 4 +- site/includes/header_video.php | 2 +- site/includes/include.php | 5 +- site/index.php | 82 ++++++++-------- site/seo_files.php | 38 ++++++++ 11 files changed, 317 insertions(+), 57 deletions(-) create mode 100644 site/seo_files.php diff --git a/.htaccess b/.htaccess index d2f4cfb..0ed500c 100644 --- a/.htaccess +++ b/.htaccess @@ -40,6 +40,8 @@ RewriteRule ^home/inscricao/?$ site/inscricao.php [L,QSA] RewriteRule ^home/crossfit/?$ site/crossfit.php [L,QSA] RewriteRule ^home/privacidade/?$ site/privacidade.php [L,QSA] RewriteRule ^evento/([a-zA-Z0-9-]+)/?$ site/evento.php?slug=$1 [L,QSA] +RewriteRule ^robots\.txt$ site/seo_files.php?f=robots [L] +RewriteRule ^sitemap\.xml$ site/seo_files.php?f=sitemap [L] # Pass the Authorization header through to PHP RewriteCond %{HTTP:Authorization} . diff --git a/config/functions_site.php b/config/functions_site.php index b32b669..49867f0 100644 --- a/config/functions_site.php +++ b/config/functions_site.php @@ -101,3 +101,26 @@ function site_horario(PDO $pdo, string $local = ''): array { 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; +} diff --git a/site/_open.php b/site/_open.php index 08a1c38..b067a0c 100644 --- a/site/_open.php +++ b/site/_open.php @@ -13,3 +13,6 @@ $GLOBALS['_view_shared'] = compact('info', 'navbar', 'current_uri_2'); echo view('include'); // renders view('seo') inside echo view('navbar'); echo view('messenger_chat'); +// Page content landmark — closed at the top of site/includes/footer.php, so +// every public page gets one
(and the skip link a real target). +echo '
' . "\n"; diff --git a/site/assets/css/style.css b/site/assets/css/style.css index 4c3a9e9..2c88619 100644 --- a/site/assets/css/style.css +++ b/site/assets/css/style.css @@ -581,9 +581,19 @@ section[id] { background: var(--fitme-white); } +/* Team backdrop: the warm grey of the gym's block wall in the header photo + (sampled ≈ rgb(70,66,62)), a touch darker so white names still pop. On top, + very faint mortar joints in running bond (120×60 blocks, alternate rows + offset by half a block) plus a fine concrete grain, both as inline SVG + tiles, and a soft shade at the top and bottom edges. */ .trainers-section { - background: var(--fitme-dark); position: relative; + background-color: #403d3a; + background-image: + linear-gradient(180deg, rgba(0, 0, 0, .28) 0%, transparent 18%, transparent 82%, rgba(0, 0, 0, .28) 100%), + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 .07 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"), + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='120'%3E%3Cpath d='M0 1H240M0 61H240M1 0V60M121 0V60M61 60V120M181 60V120' fill='none' stroke='rgba(0,0,0,0.09)' stroke-width='1.5'/%3E%3Cpath d='M0 3.5H240M0 63.5H240' fill='none' stroke='rgba(255,255,255,0.022)' stroke-width='1'/%3E%3C/svg%3E"); + background-size: 100% 100%, 200px 200px, 240px 120px; } .trainers-section:before { @@ -623,6 +633,59 @@ section[id] { .workhours-section { background: var(--fitme-white); + padding: 48px 0 44px; +} + +.workhours-section .section-title { + margin-bottom: 1.4rem; +} + +/* Compact opening-hours cards: dark day label on top, hours underneath, + side by side and close together (wraps to a column on phones). */ +.wh-grid { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin: 0; +} + +.wh-day { + flex: 0 1 210px; + border-radius: 6px; + overflow: hidden; + background: var(--fitme-surface); + border: 1px solid var(--fitme-surface-2); + text-align: center; +} + +.wh-day dt { + background: var(--fitme-dark-3); + color: #fff; + font-size: .9rem; + font-weight: 700; + padding: 8px 10px; +} + +.wh-day dd { + margin: 0; + padding: 7px 10px; + font-weight: 700; + font-size: 1rem; + color: var(--fitme-text); +} + +.wh-day dd + dd { + border-top: 1px solid var(--fitme-surface-2); +} + +.wh-day dd.wh-closed { + font-weight: 600; + color: var(--fitme-text-muted); +} + +@media (max-width: 575px) { + .wh-day { flex-basis: 100%; } } .map-section { @@ -698,6 +761,7 @@ section[id] { mix-blend-mode: multiply; } +.icon-box-item h3, .icon-box-item h4 { margin-bottom: .75rem; font-size: 1.15rem; @@ -819,6 +883,7 @@ section[id] { visibility: hidden; } +.trainer-item h3, .trainer-item h4 { margin-bottom: .3rem; } @@ -835,6 +900,7 @@ section[id] { } .trainers-section .section-title h2, +.trainers-section .trainer-item h3, .trainers-section .trainer-item h4, .trainers-section .trainer-item>*:last-child { color: #fff; @@ -1136,10 +1202,16 @@ section[id] { margin-bottom: 2.5rem; } +/* The form heading is an h3 under the section's h2 — keep the h2 look. */ +h3.contact-title { + font-size: 2.25rem; +} + .contact-info-warp { margin-bottom: 2rem; } +.contact-info-warp h3, .contact-info-warp h4 { color: var(--fitme-accent); margin-bottom: .5rem; @@ -2152,6 +2224,7 @@ section[id] { max-height: 34px; } + .icon-box-item h3, .icon-box-item h4 { font-size: .95rem; } @@ -2551,6 +2624,96 @@ section[id] { display: none !important; } +/* ── Horário: compact cards (on screen, every width) ─────────────────────────── + Smaller, narrower cards and type so a day reads at a glance and three days + fit side by side on desktop. The JPG export keeps its own sizing. */ +.schedule:not(.schedule-export) .schedule_cell { + gap: 6px; + margin-bottom: .6rem; +} + +.schedule:not(.schedule-export) .schedule_cell > .schedule_box { + flex: 0 1 112px; + padding: 6px 3px; +} + +.schedule:not(.schedule-export) .schedule_box img { + height: 22px; + margin-bottom: 2px; +} + +.schedule:not(.schedule-export) .schedule_box a { + font-size: 10px; + min-height: 2.3em; +} + +.schedule:not(.schedule-export) .schedule_info { + margin: auto -3px -6px; + padding: 4px 6px; + font-size: 11.5px; + min-height: 2.4em; +} + +.schedule:not(.schedule-export) .schedule_box:hover { + transform: scale(1.05); +} + +.schedule:not(.schedule-export) .schedule_dayhead { + margin-bottom: .7rem; +} + +.schedule:not(.schedule-export) .rounded_frame { + width: 104px; + height: 32px; + font-size: .95rem; +} + +.schedule:not(.schedule-export) .schedule_section_label { + margin: .5rem 0 .8rem; +} + +.schedule:not(.schedule-export) .schedule_section_label span { + font-size: .72rem; +} + +/* ── Desktop: yesterday · today · tomorrow ───────────────────────────────────── + ≥992px, main.js shows three days — the chosen one (today by default) centred + between its neighbours, wrapping round the week — and tags each visible + header/cell with .sched-pos-1/2/3 for its column. Rows stay time-aligned, so + classes at the same hour line up across the three days. The side days are + toned down so the centre one leads. */ +@media (min-width: 992px) { + .schedule.is-3day:not(.schedule-export) .schedule_panel { + grid-template-columns: repeat(3, minmax(0, 420px)); + column-gap: 28px; + } + .schedule.is-3day:not(.schedule-export) .schedule_section_label { + grid-column: 1 / -1; + } + .schedule.is-3day:not(.schedule-export) .sched-pos-1 { grid-column: 1 !important; } + .schedule.is-3day:not(.schedule-export) .sched-pos-2 { grid-column: 2 !important; } + .schedule.is-3day:not(.schedule-export) .sched-pos-3 { grid-column: 3 !important; } + + .schedule.is-3day:not(.schedule-export) .schedule_cell.sched-side { + opacity: .5; + transition: opacity var(--fitme-transition); + } + .schedule.is-3day:not(.schedule-export) .schedule_cell.sched-side:hover { + opacity: 1; + } + .schedule.is-3day:not(.schedule-export) .schedule_dayhead.sched-side .rounded_frame { + background: var(--fitme-surface-2); + color: var(--fitme-text-muted); + } + .schedule.is-3day:not(.schedule-export) .schedule_dayhead.sched-pos-2 .rounded_frame { + background: var(--fitme-accent); + color: var(--fitme-dark); + } + .schedule.is-3day:not(.schedule-export) .schedule_dayhead.sched-side { + cursor: pointer; + } +} + /* ── Aula experimental modal ───────────────────────────────────────────────── */ .modal-content.trial-modal { background: #fff !important; @@ -2963,6 +3126,7 @@ section[id] { /* The name used to be the tile's last child and got its size from the `.trainer-item>*:last-child` rule; pin it so it stays the same whether or not a title follows it. */ +.trainers-section .trainer-item h3, .trainers-section .trainer-item h4 { font-size: .9rem; } diff --git a/site/assets/js/main.js b/site/assets/js/main.js index e7a3e6a..c6c8b33 100644 --- a/site/assets/js/main.js +++ b/site/assets/js/main.js @@ -152,10 +152,11 @@ document.addEventListener('DOMContentLoaded', function () { }); /* ── Schedule day filter ───────────────────────────────────────────────────── - The on-screen schedule shows one day at a time; a day-picker bar switches days - (each header + class cell is tagged with data-day). Defaults to today when the - gym is open that day, otherwise the first day. The JPG export is unaffected — - its clone removes every .sched-hide and re-renders the full week. */ + The on-screen schedule shows the chosen day (phones) or that day centred + between the day before and after (desktop ≥992px); a day-picker bar switches + days (each header + class cell is tagged with data-day). Defaults to today + when the gym is open that day, otherwise the first day. The JPG export is + unaffected — its clone removes every .sched-hide and re-renders the full week. */ document.addEventListener('DOMContentLoaded', function () { // Works on the home page (#horario) and the CrossFit page (bare .schedule). var scope = document.querySelector('.schedule'); @@ -166,19 +167,40 @@ document.addEventListener('DOMContentLoaded', function () { var buttons = filter.querySelectorAll('.sdf-btn'); var labels = scope.querySelectorAll('.schedule_section_label'); - // Toggle a class (not inline display) — .sched-hide carries !important so it - // reliably wins, and the export clone simply strips it to show every day. + // Desktop shows three days — the chosen one centred between the day before + // and the day after (wrapping round the week); phones show just the one. + var wide = window.matchMedia('(min-width: 992px)'); + var week = Array.prototype.map.call(buttons, function (b) { return b.getAttribute('data-day'); }); + var current = null; + + // Toggle classes (not inline display) — .sched-hide carries !important so it + // reliably wins, and the export clone simply strips it (and ignores the + // .sched-pos-* columns) to show every day. function showDay(day) { + current = day; + var i = week.indexOf(day), n = week.length; + var three = wide.matches && n >= 3; + var visible = three ? [week[(i - 1 + n) % n], day, week[(i + 1) % n]] : [day]; + scope.classList.toggle('is-3day', three); cells.forEach(function (c) { - c.classList.toggle('sched-hide', c.getAttribute('data-day') !== day); + var pos = visible.indexOf(c.getAttribute('data-day')); + c.classList.toggle('sched-hide', pos === -1); + c.classList.remove('sched-pos-1', 'sched-pos-2', 'sched-pos-3', 'sched-side'); + if (pos !== -1) { + var col = three ? pos + 1 : 1; + c.classList.add('sched-pos-' + col); + if (three && col !== 2) c.classList.add('sched-side'); + } }); buttons.forEach(function (b) { b.classList.toggle('active', b.getAttribute('data-day') === day); }); - // Show a Manhã / Tarde divider only when the day has a class in that block. + // Show a Manhã / Tarde divider only when a visible day has a class in that block. labels.forEach(function (lbl) { var sec = lbl.getAttribute('data-section'); - var has = !!scope.querySelector('.schedule_cell[data-day="' + day + '"][data-section="' + sec + '"]'); + var has = visible.some(function (d) { + return !!scope.querySelector('.schedule_cell[data-day="' + d + '"][data-section="' + sec + '"]'); + }); lbl.classList.toggle('sched-hide', !has); }); } @@ -187,6 +209,13 @@ document.addEventListener('DOMContentLoaded', function () { var btn = e.target.closest('.sdf-btn'); if (btn) showDay(btn.getAttribute('data-day')); }); + // Clicking a side day's header centres it. + scope.addEventListener('click', function (e) { + var head = e.target.closest('.schedule_dayhead.sched-side'); + if (head) showDay(head.getAttribute('data-day')); + }); + var reflow = function () { if (current) showDay(current); }; + if (wide.addEventListener) wide.addEventListener('change', reflow); else if (wide.addListener) wide.addListener(reflow); // Pre-select today when the schedule has a button for it (PT day names), // otherwise the first day. diff --git a/site/includes/footer.php b/site/includes/footer.php index 438be4a..f129bf4 100644 --- a/site/includes/footer.php +++ b/site/includes/footer.php @@ -1,3 +1,5 @@ +
+