feat: borda verde a rodar nos planos, textura metálica, contactos numa só fonte
- Mensalidades: ao passar o rato, a borda do cartão ganha um brilho verde num conic-gradient que roda (@property --pc-a animável), centrado no cursor; o cartão inclina-se e desliza para o rato como os da equipa. - «O que dizem de nós» e rodapé: textura discreta de metal preto mate (grão de pintura eletrostática, brilho difuso, riscas quase invisíveis), em SVG/CSS. - Contactos: o site lê só fitme3070_info (Gestão → Contactos). Os campos contact_*/social_* de Sistema → Definições não eram usados e foram editados por engano — saem do formulário, com um aviso a apontar para Contactos. site_info() deixa o WhatsApp só com dígitos (wa.me não aceita + nem espaços). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
21ff1b55ff
commit
0587be7d14
4 files changed
+166
-78
No files matched your search
@@ -16,6 +16,8 @@ function site_info(PDO $pdo): array {
|
|||||||
foreach ($cols as $c) {
|
foreach ($cols as $c) {
|
||||||
if (!array_key_exists($c, $row) || $row[$c] === null) $row[$c] = '';
|
if (!array_key_exists($c, $row) || $row[$c] === null) $row[$c] = '';
|
||||||
}
|
}
|
||||||
|
// wa.me only accepts digits — a stray "+" or space would break the link.
|
||||||
|
$row['whatsapp'] = preg_replace('/\D+/', '', (string) $row['whatsapp']);
|
||||||
return $row;
|
return $row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+115
-13
@@ -1380,11 +1380,15 @@ h3.contact-title {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
background:
|
/* Same matte-metal texture as the reviews section, over the (now barely
|
||||||
|
visible) gym photo. */
|
||||||
|
background-image:
|
||||||
|
var(--metal-layers),
|
||||||
linear-gradient(180deg,
|
linear-gradient(180deg,
|
||||||
rgba(10, 10, 10, 0.94) 0%,
|
rgba(20, 21, 23, 0.96) 0%,
|
||||||
rgba(10, 10, 10, 0.88) 50%,
|
rgba(20, 21, 23, 0.92) 50%,
|
||||||
rgba(10, 10, 10, 0.94) 100%);
|
rgba(20, 21, 23, 0.96) 100%);
|
||||||
|
background-size: var(--metal-sizes), 100% 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-section .container {
|
.footer-section .container {
|
||||||
@@ -3584,34 +3588,116 @@ h3.contact-title {
|
|||||||
gap: 24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Spinning green edge (hover). --pc-a is a registered <angle>, so the conic
|
||||||
|
gradient's start angle can be animated — an unregistered custom property
|
||||||
|
would just jump. The gradient is centred on the mouse (--pc-x/--pc-y from
|
||||||
|
main.js), so the sweep pivots around the pointer; the card also tilts and
|
||||||
|
drifts toward it (--pc-rx/ry/mx/my), like the team cards.
|
||||||
|
Layers behind the content: ::after = the blurred gradient (the glow),
|
||||||
|
::before = the card face with a 2px border whose border-box shows the crisp
|
||||||
|
gradient (padding-box/border-box backgrounds keep the rounded corners). */
|
||||||
|
@property --pc-a {
|
||||||
|
syntax: '<angle>';
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pc-spin {
|
||||||
|
to { --pc-a: 360deg; }
|
||||||
|
}
|
||||||
|
|
||||||
.price-card {
|
.price-card {
|
||||||
|
--pc-face: #fff;
|
||||||
|
--pc-edge: var(--fitme-surface-2);
|
||||||
|
--pc-bw: 1px;
|
||||||
|
--pc-scale: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
flex: 0 1 270px;
|
flex: 0 1 270px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: #fff;
|
background: var(--pc-face);
|
||||||
border: 1px solid var(--fitme-surface-2);
|
border: 1px solid transparent;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 30px 26px 26px;
|
padding: 30px 26px 26px;
|
||||||
box-shadow: var(--fitme-shadow);
|
box-shadow: var(--fitme-shadow);
|
||||||
transition: transform var(--fitme-transition), box-shadow var(--fitme-transition);
|
transform: perspective(900px) rotateX(var(--pc-rx, 0deg)) rotateY(var(--pc-ry, 0deg))
|
||||||
|
translate(var(--pc-mx, 0px), calc(var(--pc-my, 0px) + var(--pc-lift, 0px))) scale(var(--pc-scale));
|
||||||
|
transition: transform .45s cubic-bezier(.34, 1.35, .64, 1), box-shadow var(--fitme-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* While the pointer is on it, follow quickly instead of springing. */
|
||||||
|
.price-card.pc-tracking {
|
||||||
|
transition: transform .15s ease-out, box-shadow var(--fitme-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-card::before,
|
||||||
|
.price-card::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-card::before {
|
||||||
|
inset: calc(-1 * var(--pc-bw));
|
||||||
|
z-index: -1;
|
||||||
|
border-radius: calc(6px + var(--pc-bw));
|
||||||
|
border: var(--pc-bw) solid transparent;
|
||||||
|
background:
|
||||||
|
linear-gradient(var(--pc-face), var(--pc-face)) padding-box,
|
||||||
|
linear-gradient(var(--pc-edge), var(--pc-edge)) border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-card::after {
|
||||||
|
inset: -3px;
|
||||||
|
z-index: -2;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: conic-gradient(from var(--pc-a) at var(--pc-x, 50%) var(--pc-y, 50%),
|
||||||
|
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);
|
||||||
|
filter: blur(12px);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity .35s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-card:hover {
|
.price-card:hover {
|
||||||
transform: translateY(-4px);
|
--pc-lift: -4px;
|
||||||
|
--pc-bw: 2px;
|
||||||
box-shadow: var(--fitme-shadow-lg);
|
box-shadow: var(--fitme-shadow-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.price-card:hover::before {
|
||||||
|
background:
|
||||||
|
linear-gradient(var(--pc-face), var(--pc-face)) padding-box,
|
||||||
|
conic-gradient(from var(--pc-a) at var(--pc-x, 50%) var(--pc-y, 50%),
|
||||||
|
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;
|
||||||
|
animation: pc-spin 3.2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-card:hover::after {
|
||||||
|
opacity: .75;
|
||||||
|
animation: pc-spin 3.2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.price-card.is-featured {
|
.price-card.is-featured {
|
||||||
background: var(--fitme-dark-2);
|
--pc-face: var(--fitme-dark-2);
|
||||||
border: 2px solid var(--fitme-accent);
|
--pc-edge: var(--fitme-accent);
|
||||||
|
--pc-bw: 2px;
|
||||||
|
border-width: 2px;
|
||||||
color: var(--fitme-ash);
|
color: var(--fitme-ash);
|
||||||
box-shadow: 0 18px 50px rgba(var(--fitme-dark-rgb), .28);
|
box-shadow: 0 18px 50px rgba(var(--fitme-dark-rgb), .28);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
.price-card.is-featured { transform: scale(1.04); }
|
.price-card.is-featured { --pc-scale: 1.04; }
|
||||||
.price-card.is-featured:hover { transform: scale(1.04) translateY(-4px); }
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.price-card:hover::before,
|
||||||
|
.price-card:hover::after { animation: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.pc-badge {
|
.pc-badge {
|
||||||
@@ -3731,9 +3817,25 @@ h3.contact-title {
|
|||||||
/* ══════════════════════════════════════════════════════════════════════════
|
/* ══════════════════════════════════════════════════════════════════════════
|
||||||
Reviews Google
|
Reviews Google
|
||||||
══════════════════════════════════════════════════════════════════════════ */
|
══════════════════════════════════════════════════════════════════════════ */
|
||||||
|
/* Matte black "gym machine" metal, shared by the reviews section and the
|
||||||
|
footer: fine powder-coat grain (light + dark speckle from SVG turbulence),
|
||||||
|
a soft diffuse sheen like light on a frame tube, and barely-there brushed
|
||||||
|
streaks. All inline, no image requests. */
|
||||||
|
:root {
|
||||||
|
--metal-layers:
|
||||||
|
radial-gradient(120% 70% at 22% 0%, rgba(255, 255, 255, .055), transparent 60%),
|
||||||
|
linear-gradient(112deg, transparent 38%, rgba(255, 255, 255, .03) 50%, transparent 62%),
|
||||||
|
repeating-linear-gradient(0deg, rgba(255, 255, 255, .012) 0 1px, transparent 1px 3px),
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' 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 .09 -.03'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E"),
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='d'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='1' seed='7' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .22 -.06'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23d)'/%3E%3C/svg%3E");
|
||||||
|
--metal-sizes: 100% 100%, 100% 100%, 100% 3px, 180px 180px, 140px 140px;
|
||||||
|
}
|
||||||
|
|
||||||
.reviews-section {
|
.reviews-section {
|
||||||
padding: 90px 0 60px;
|
padding: 90px 0 60px;
|
||||||
background: var(--fitme-dark);
|
background-color: #141517;
|
||||||
|
background-image: var(--metal-layers);
|
||||||
|
background-size: var(--metal-sizes);
|
||||||
color: var(--fitme-ash);
|
color: var(--fitme-ash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -640,6 +640,47 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
window.addEventListener('resize', function () { closeAll(null); }, { passive: true });
|
window.addEventListener('resize', function () { closeAll(null); }, { passive: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ── Mensalidades: cards follow the mouse ───────────────────────────────────
|
||||||
|
While hovered, a price card tilts and drifts a few px toward the pointer
|
||||||
|
(like the team cards) and the spinning conic edge is centred on it
|
||||||
|
(--pc-x/--pc-y feed the gradient's `at` position). Pointer devices only;
|
||||||
|
off with reduced motion. */
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
var cards = document.querySelectorAll('.price-card');
|
||||||
|
if (!cards.length) return;
|
||||||
|
var fine = window.matchMedia('(hover: hover) and (pointer: fine)');
|
||||||
|
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||||
|
var VARS = ['--pc-rx', '--pc-ry', '--pc-mx', '--pc-my', '--pc-x', '--pc-y'];
|
||||||
|
|
||||||
|
cards.forEach(function (card) {
|
||||||
|
var t;
|
||||||
|
card.addEventListener('mouseenter', function () {
|
||||||
|
if (!fine.matches || reduced.matches) return;
|
||||||
|
clearTimeout(t);
|
||||||
|
t = setTimeout(function () { card.classList.add('pc-tracking'); }, 350);
|
||||||
|
});
|
||||||
|
card.addEventListener('mousemove', function (e) {
|
||||||
|
if (!fine.matches || reduced.matches) return;
|
||||||
|
var r = card.getBoundingClientRect();
|
||||||
|
var x = e.clientX - r.left, y = e.clientY - r.top;
|
||||||
|
var dx = Math.max(-1, Math.min(1, (x - r.width / 2) / (r.width / 2)));
|
||||||
|
var dy = Math.max(-1, Math.min(1, (y - r.height / 2) / (r.height / 2)));
|
||||||
|
card.style.setProperty('--pc-ry', (dx * 6).toFixed(2) + 'deg');
|
||||||
|
card.style.setProperty('--pc-rx', (-dy * 6).toFixed(2) + 'deg');
|
||||||
|
card.style.setProperty('--pc-mx', (dx * 6).toFixed(1) + 'px');
|
||||||
|
card.style.setProperty('--pc-my', (dy * 6).toFixed(1) + 'px');
|
||||||
|
// The pseudo-elements sit a few px outside the card; close enough.
|
||||||
|
card.style.setProperty('--pc-x', x.toFixed(0) + 'px');
|
||||||
|
card.style.setProperty('--pc-y', y.toFixed(0) + 'px');
|
||||||
|
});
|
||||||
|
card.addEventListener('mouseleave', function () {
|
||||||
|
clearTimeout(t);
|
||||||
|
card.classList.remove('pc-tracking'); // spring back
|
||||||
|
VARS.forEach(function (v) { card.style.removeProperty(v); });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/* ── Mensalidades: pre-select the clicked card in the sign-up modal ───────── */
|
/* ── Mensalidades: pre-select the clicked card in the sign-up modal ───────── */
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
var select = document.querySelector('#joinModal select[name="membership_id"]');
|
var select = document.querySelector('#joinModal select[name="membership_id"]');
|
||||||
|
|||||||
@@ -17,14 +17,10 @@ $fields = [
|
|||||||
'email_from_name' => ['label' => 'Nome do remetente', 'type' => 'text', 'required' => true],
|
'email_from_name' => ['label' => 'Nome do remetente', 'type' => 'text', 'required' => true],
|
||||||
'email_subject_prefix' => ['label' => 'Prefixo do assunto', 'type' => 'text', 'required' => false],
|
'email_subject_prefix' => ['label' => 'Prefixo do assunto', 'type' => 'text', 'required' => false],
|
||||||
'reminder_hours' => ['label' => 'Horas de reminder', 'type' => 'number','required' => true],
|
'reminder_hours' => ['label' => 'Horas de reminder', 'type' => 'number','required' => true],
|
||||||
'contact_email' => ['label' => 'Email de contacto público', 'type' => 'email', 'required' => false],
|
// Public contacts are NOT here: the site reads them from fitme3070_info,
|
||||||
'contact_phone_display'=> ['label' => 'Telefone (texto exibido)', 'type' => 'text', 'required' => false],
|
// edited in Gestão → Contactos (views/admin/info/edit.php). The fleet
|
||||||
'contact_phone_dial' => ['label' => 'Telefone (formato dial)', 'type' => 'text', 'required' => false],
|
// template's contact_* / social_* settings were unused and got edited by
|
||||||
'contact_phone_intl' => ['label' => 'Telefone (formato internacional, sem +)', 'type' => 'text', 'required' => false],
|
// mistake, so they were removed from this form.
|
||||||
'contact_address_line1'=> ['label' => 'Morada — linha 1', 'type' => 'text', 'required' => false],
|
|
||||||
'contact_address_line2'=> ['label' => 'Morada — linha 2', 'type' => 'text', 'required' => false],
|
|
||||||
'social_facebook_url' => ['label' => 'Facebook URL', 'type' => 'url', 'required' => false],
|
|
||||||
'social_instagram_url' => ['label' => 'Instagram URL', 'type' => 'url', 'required' => false],
|
|
||||||
|
|
||||||
// SEO defaults — used by site/includes/head.php when a page doesn't
|
// SEO defaults — used by site/includes/head.php when a page doesn't
|
||||||
// set its own $page_title / $page_description / $page_image override.
|
// set its own $page_title / $page_description / $page_image override.
|
||||||
@@ -226,63 +222,10 @@ include __DIR__ . '/../../includes/navbar.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card shadow-sm mb-3">
|
<div class="alert alert-info d-flex align-items-center gap-2 mb-3">
|
||||||
<div class="card-header"><strong>Contactos públicos</strong></div>
|
<i class="bi bi-telephone-fill"></i>
|
||||||
<div class="card-body">
|
<div>Telefone, WhatsApp, e-mail, morada e redes sociais do site editam-se em
|
||||||
<div class="row g-3">
|
<a href="<?= base_url('views/admin/info/edit.php') ?>" class="alert-link">Gestão → Contactos</a>.</div>
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label small">Email de contacto</label>
|
|
||||||
<input type="email" name="contact_email" class="form-control"
|
|
||||||
value="<?= htmlspecialchars($values['contact_email']) ?>"
|
|
||||||
placeholder="andre@andremoreira.pt">
|
|
||||||
<div class="form-text">Mostrado no site e recebe os pedidos de orçamento e contacto.</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label small">Telefone (texto exibido)</label>
|
|
||||||
<input type="text" name="contact_phone_display" class="form-control"
|
|
||||||
value="<?= htmlspecialchars($values['contact_phone_display']) ?>"
|
|
||||||
placeholder="915 435 034">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label small">Telefone (dial / <code>tel:</code>)</label>
|
|
||||||
<input type="text" name="contact_phone_dial" class="form-control font-monospace"
|
|
||||||
value="<?= htmlspecialchars($values['contact_phone_dial']) ?>"
|
|
||||||
placeholder="+351915435034">
|
|
||||||
<div class="form-text">Formato E.164 (com <code>+</code>).</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label small">Telefone (formato WhatsApp <code>wa.me</code>)</label>
|
|
||||||
<input type="text" name="contact_phone_intl" class="form-control font-monospace"
|
|
||||||
value="<?= htmlspecialchars($values['contact_phone_intl']) ?>"
|
|
||||||
placeholder="351915435034">
|
|
||||||
<div class="form-text">Sem <code>+</code> nem espaços. Usado em <code>https://wa.me/<numero></code>.</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label small">Morada — linha 1</label>
|
|
||||||
<input type="text" name="contact_address_line1" class="form-control"
|
|
||||||
value="<?= htmlspecialchars($values['contact_address_line1']) ?>"
|
|
||||||
placeholder="Av. Cidade de Coimbra 159">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label small">Morada — linha 2</label>
|
|
||||||
<input type="text" name="contact_address_line2" class="form-control"
|
|
||||||
value="<?= htmlspecialchars($values['contact_address_line2']) ?>"
|
|
||||||
placeholder="3070-761 Praia de Mira">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label small">Facebook URL</label>
|
|
||||||
<input type="url" name="social_facebook_url" class="form-control"
|
|
||||||
value="<?= htmlspecialchars($values['social_facebook_url']) ?>"
|
|
||||||
placeholder="https://www.facebook.com/...">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label small">Instagram URL</label>
|
|
||||||
<input type="url" name="social_instagram_url" class="form-control"
|
|
||||||
value="<?= htmlspecialchars($values['social_instagram_url']) ?>"
|
|
||||||
placeholder="https://www.instagram.com/...">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card shadow-sm mb-3">
|
<div class="card shadow-sm mb-3">
|
||||||
|
|||||||
Reference in new issue
Block a user