790 lines
39 KiB
JavaScript
790 lines
39 KiB
JavaScript
'use strict';
|
||
|
||
/* ── Lazy background images (.set-bg / data-setbg) ──────────────────────
|
||
Backgrounds are applied as their element approaches the viewport, so
|
||
below-the-fold images (contactos, newsletter, footer) don't compete
|
||
with the hero for bandwidth. */
|
||
(function () {
|
||
var els = Array.prototype.slice.call(document.querySelectorAll('.set-bg'));
|
||
function apply(el) {
|
||
var bg = el.getAttribute('data-setbg');
|
||
if (bg) el.style.backgroundImage = "url('" + bg + "')";
|
||
}
|
||
if ('IntersectionObserver' in window) {
|
||
var io = new IntersectionObserver(function (entries) {
|
||
entries.forEach(function (entry) {
|
||
if (entry.isIntersecting) {
|
||
apply(entry.target);
|
||
io.unobserve(entry.target);
|
||
}
|
||
});
|
||
}, { rootMargin: '300px 0px' });
|
||
els.forEach(function (el) { io.observe(el); });
|
||
} else {
|
||
els.forEach(apply);
|
||
}
|
||
})();
|
||
|
||
/* ── Back to top button ─────────────────────────────────────────────── */
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
var btn = document.getElementById('backToTop');
|
||
if (!btn) return;
|
||
|
||
function toggleBtn() {
|
||
if (window.scrollY > 400) btn.classList.add('visible');
|
||
else btn.classList.remove('visible');
|
||
}
|
||
window.addEventListener('scroll', toggleBtn, { passive: true });
|
||
btn.addEventListener('click', function () {
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
});
|
||
});
|
||
|
||
|
||
/* Scroll-reveal: elements fade + slide up as they enter viewport */
|
||
if ('IntersectionObserver' in window) {
|
||
(function () {
|
||
var observer = new IntersectionObserver(function (entries) {
|
||
entries.forEach(function (entry) {
|
||
if (entry.isIntersecting) {
|
||
entry.target.classList.add('revealed');
|
||
observer.unobserve(entry.target);
|
||
}
|
||
});
|
||
}, { rootMargin: '0px 0px -60px 0px', threshold: 0.05 });
|
||
|
||
// Target all cards, sections, and content blocks
|
||
document.querySelectorAll(
|
||
'section[id], .icon-box-item, .trainer-item, .schedule_box, ' +
|
||
'.classes-section .col-lg-6, .contact-info-warp, .rounded_frame_grey'
|
||
).forEach(function (el) { observer.observe(el); });
|
||
|
||
// Hero reveals immediately after preloader
|
||
window.addEventListener('load', function () {
|
||
var hero = document.querySelector('.hero-section');
|
||
if (hero) hero.classList.add('revealed');
|
||
});
|
||
})();
|
||
}
|
||
|
||
/* Hide scroll hint after user scrolls the schedule */
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
var panel = document.querySelector('.schedule_panel');
|
||
var hint = document.querySelector('.schedule-scroll-hint');
|
||
if (panel && hint) {
|
||
panel.addEventListener('scroll', function () {
|
||
hint.style.opacity = '0';
|
||
hint.style.transition = 'opacity .3s';
|
||
setTimeout(function () { hint.style.display = 'none'; }, 300);
|
||
}, { once: true });
|
||
}
|
||
});
|
||
|
||
/* ── Modalidades: scrollable strip + detail card ────────────────────────────
|
||
.mod-scroll is a native horizontal scroller, so touch, trackpad and
|
||
shift+wheel just work; on top of that: mouse drag, arrow buttons, plain
|
||
wheel → sideways, and a slow auto-glide (~20px/s) that pauses while anyone
|
||
interacts and picks up again a moment later. The card set is cloned once so
|
||
the glide loops: past the first copy we jump back by exactly its width.
|
||
(The position is kept in a float — assigning scrollLeft += 0.3 each frame
|
||
rounds to nothing — and written back every frame.)
|
||
Cards with a description open .mod-detail, one shared panel outside the
|
||
scroller (so overflow doesn't clip it) that grows out of the card like the
|
||
team cards do, with the same 3D tilt toward the mouse; on touch it flies to
|
||
the centre of the screen instead. */
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
var section = document.getElementById('modalidades');
|
||
var scroller = section && section.querySelector('.mod-scroll');
|
||
if (!scroller) return;
|
||
var panel = section.querySelector('.mod-detail');
|
||
var floating = window.matchMedia('(max-width: 767px), (hover: none)');
|
||
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||
|
||
// ── Loop: clone the set once ──────────────────────────────────────────
|
||
var originals = Array.prototype.slice.call(scroller.querySelectorAll('.mod-card'));
|
||
originals.forEach(function (c) {
|
||
var clone = c.cloneNode(true);
|
||
clone.setAttribute('aria-hidden', 'true');
|
||
clone.setAttribute('tabindex', '-1');
|
||
clone.classList.add('is-clone');
|
||
scroller.appendChild(clone);
|
||
});
|
||
function loopWidth() {
|
||
// Distance from the first original to the first clone = one full set.
|
||
var first = scroller.querySelector('.mod-card');
|
||
var firstClone = scroller.querySelector('.mod-card.is-clone');
|
||
return first && firstClone ? firstClone.offsetLeft - first.offsetLeft : 0;
|
||
}
|
||
function wrap() {
|
||
var w = loopWidth();
|
||
if (!w) return;
|
||
if (scroller.scrollLeft >= w) { scroller.scrollLeft -= w; pos = scroller.scrollLeft; }
|
||
else if (scroller.scrollLeft <= 0 && (performance.now() - lastUser) < IDLE) { scroller.scrollLeft += w; pos = scroller.scrollLeft; }
|
||
}
|
||
|
||
// ── Auto-glide ────────────────────────────────────────────────────────
|
||
var SPEED = 20; // px per second
|
||
var IDLE = 2500; // ms after the last interaction before gliding again
|
||
var pos = 0, last = 0, lastUser = -1e9, hovering = false; // -1e9: glide from the start
|
||
function busy() {
|
||
return hovering || dragging || panelOpen() || document.hidden ||
|
||
(performance.now() - lastUser) < IDLE;
|
||
}
|
||
function tick(t) {
|
||
var dt = last ? Math.min(64, t - last) : 0;
|
||
last = t;
|
||
if (!busy()) {
|
||
pos += SPEED * dt / 1000;
|
||
scroller.scrollLeft = pos;
|
||
wrap();
|
||
} else {
|
||
pos = scroller.scrollLeft;
|
||
}
|
||
requestAnimationFrame(tick);
|
||
}
|
||
requestAnimationFrame(tick);
|
||
function touched() { lastUser = performance.now(); }
|
||
|
||
scroller.addEventListener('mouseenter', function () { hovering = true; });
|
||
scroller.addEventListener('mouseleave', function () { hovering = false; touched(); });
|
||
scroller.addEventListener('touchstart', touched, { passive: true });
|
||
scroller.addEventListener('touchmove', touched, { passive: true });
|
||
scroller.addEventListener('focusin', touched);
|
||
scroller.addEventListener('scroll', function () {
|
||
if (dragging || (performance.now() - lastUser) < IDLE) { touched(); wrap(); }
|
||
if (panelOpen() && !opening) closePanel();
|
||
}, { passive: true });
|
||
|
||
// Plain vertical wheel over the strip scrolls it sideways — but only while
|
||
// there is room, so the page keeps scrolling past it at the ends… which a
|
||
// looping strip never has, so: only when the pointer is actually on it.
|
||
scroller.addEventListener('wheel', function (e) {
|
||
if (floating.matches || Math.abs(e.deltaX) > Math.abs(e.deltaY) || e.shiftKey) { touched(); return; }
|
||
e.preventDefault();
|
||
touched();
|
||
scroller.scrollLeft += e.deltaY;
|
||
wrap();
|
||
}, { passive: false });
|
||
|
||
// ── Mouse drag ────────────────────────────────────────────────────────
|
||
var dragging = false, dragStartX = 0, dragStartLeft = 0, dragMoved = 0, suppressClick = false;
|
||
scroller.addEventListener('pointerdown', function (e) {
|
||
if (e.pointerType !== 'mouse' || e.button !== 0) return;
|
||
dragging = true; dragMoved = 0;
|
||
dragStartX = e.clientX; dragStartLeft = scroller.scrollLeft;
|
||
touched();
|
||
});
|
||
window.addEventListener('pointermove', function (e) {
|
||
if (!dragging) return;
|
||
var dx = e.clientX - dragStartX;
|
||
dragMoved = Math.max(dragMoved, Math.abs(dx));
|
||
if (dragMoved > 5) {
|
||
scroller.classList.add('is-dragging');
|
||
scroller.scrollLeft = dragStartLeft - dx;
|
||
wrap();
|
||
touched();
|
||
}
|
||
});
|
||
window.addEventListener('pointerup', function () {
|
||
if (!dragging) return;
|
||
dragging = false;
|
||
suppressClick = dragMoved > 5;
|
||
scroller.classList.remove('is-dragging');
|
||
touched();
|
||
});
|
||
// A drag must not also count as a click on the card under the pointer.
|
||
scroller.addEventListener('click', function (e) {
|
||
if (suppressClick) { e.stopPropagation(); e.preventDefault(); suppressClick = false; }
|
||
}, true);
|
||
|
||
// ── Arrow buttons ─────────────────────────────────────────────────────
|
||
function step(dir) {
|
||
var card = scroller.querySelector('.mod-card');
|
||
var by = card ? (card.offsetWidth + 8) * Math.max(1, Math.floor(scroller.clientWidth / (card.offsetWidth + 8)) - 1) : 400;
|
||
touched();
|
||
if (dir < 0 && scroller.scrollLeft - by < 0) { scroller.scrollLeft += loopWidth(); }
|
||
scroller.scrollBy({ left: dir * by, behavior: reduced.matches ? 'auto' : 'smooth' });
|
||
}
|
||
section.querySelectorAll('.mod-nav').forEach(function (b) {
|
||
b.addEventListener('click', function (e) {
|
||
e.stopPropagation();
|
||
closePanel();
|
||
step(b.classList.contains('mod-nav-prev') ? -1 : 1);
|
||
});
|
||
});
|
||
|
||
// ── Detail panel ──────────────────────────────────────────────────────
|
||
if (!panel) return;
|
||
var source = null, opening = false, trackTimer = 0, backdrop = null;
|
||
var W = 540, GUTTER = 16;
|
||
function panelOpen() { return panel.classList.contains('is-open'); }
|
||
|
||
function fill(card) {
|
||
var name = card.querySelector('h3').textContent;
|
||
var tpl = card.querySelector('template.mod-desc');
|
||
// Same icon markup as the card (inline animated SVG or <img>), so the
|
||
// panel's copy replays its animation as it opens.
|
||
var icon = card.querySelector('.ib-icon');
|
||
panel.querySelector('.md-card .ib-icon').innerHTML = icon ? icon.innerHTML : '';
|
||
panel.querySelector('.md-card h3').textContent = name;
|
||
panel.querySelector('.md-name').textContent = name;
|
||
panel.querySelector('.md-text').innerHTML = tpl ? tpl.innerHTML : '';
|
||
panel.style.setProperty('--md-color', card.getAttribute('data-color') || '');
|
||
panel.setAttribute('aria-label', name);
|
||
}
|
||
|
||
// Desktop: lay the panel's left column exactly over the card.
|
||
function place(card) {
|
||
var sr = section.getBoundingClientRect(), cr = card.getBoundingClientRect();
|
||
var vw = document.documentElement.clientWidth;
|
||
var w = Math.min(W, vw - GUTTER * 2);
|
||
var flip = cr.left + w > vw - GUTTER;
|
||
var x = (flip ? cr.right - w : cr.left) - sr.left;
|
||
var icon = card.querySelector('.ib-icon');
|
||
panel.classList.toggle('md-flip', flip);
|
||
panel.style.setProperty('--md-w', w + 'px');
|
||
panel.style.setProperty('--md-cw', cr.width + 'px');
|
||
panel.style.setProperty('--md-ch', cr.height + 'px');
|
||
panel.style.setProperty('--md-pad', (icon ? icon.offsetTop : 0) + 'px');
|
||
panel.style.setProperty('--md-x', x + 'px');
|
||
panel.style.setProperty('--md-y', (cr.top - sr.top) + 'px');
|
||
panel.style.setProperty('--md-cx', Math.max(0, w - cr.width) + 'px');
|
||
panel.style.setProperty('--md-cy', Math.max(0, panel.offsetHeight - cr.height) + 'px');
|
||
// Open, the icon + name slide from where they sit on the card to the
|
||
// vertical centre of the (taller) panel column.
|
||
var title = card.querySelector('h3');
|
||
var contentH = icon && title ? (title.offsetTop + title.offsetHeight) - icon.offsetTop : 0;
|
||
panel.style.setProperty('--md-pad-open', Math.max(0, (panel.offsetHeight - contentH) / 2) + 'px');
|
||
}
|
||
|
||
function flyFrom(card, opening) {
|
||
if (!panel.animate) return null;
|
||
var c = card.getBoundingClientRect(), p = panel.getBoundingClientRect();
|
||
if (!p.width || !p.height) return null;
|
||
var from = 'translate(' + (c.left - p.left) + 'px,' + (c.top - p.top) + 'px) scale(' +
|
||
(c.width / p.width) + ',' + (c.height / p.height) + ')';
|
||
var frames = [{ transform: from, opacity: .4 }, { transform: 'none', opacity: 1 }];
|
||
return panel.animate(opening ? frames : frames.reverse(), {
|
||
duration: opening ? 420 : 260,
|
||
easing: opening ? 'cubic-bezier(.2,.9,.25,1)' : 'cubic-bezier(.4,0,1,1)'
|
||
});
|
||
}
|
||
|
||
function openPanel(card) {
|
||
if (source === card && panelOpen()) return;
|
||
closePanel(true);
|
||
source = card;
|
||
fill(card);
|
||
opening = true;
|
||
if (floating.matches) {
|
||
backdrop = document.createElement('div');
|
||
backdrop.className = 'mod-detail-backdrop';
|
||
backdrop.addEventListener('click', function () { closePanel(); });
|
||
section.appendChild(backdrop);
|
||
panel.classList.add('is-open');
|
||
panel.style.setProperty('--md-top', Math.max(24, (window.innerHeight - panel.offsetHeight) / 2) + 'px');
|
||
flyFrom(card, true);
|
||
} else {
|
||
var mdCard = panel.querySelector('.md-card');
|
||
panel.style.transition = mdCard.style.transition = 'none';
|
||
place(card);
|
||
void panel.offsetWidth;
|
||
panel.style.transition = mdCard.style.transition = '';
|
||
void panel.offsetWidth;
|
||
panel.classList.add('is-open');
|
||
card.classList.add('is-source');
|
||
trackTimer = setTimeout(function () { panel.classList.add('md-tracking'); }, 600);
|
||
}
|
||
panel.setAttribute('aria-hidden', 'false');
|
||
section.classList.add('has-open-detail');
|
||
setTimeout(function () { opening = false; }, 450);
|
||
}
|
||
|
||
function closePanel(instant) {
|
||
if (!panelOpen() && !source) return;
|
||
var card = source;
|
||
clearTimeout(trackTimer);
|
||
['--md-rx', '--md-ry', '--md-mx', '--md-my'].forEach(function (p) { panel.style.removeProperty(p); });
|
||
panel.classList.remove('md-tracking');
|
||
var done = function () {
|
||
panel.classList.remove('is-open');
|
||
if (card) card.classList.remove('is-source');
|
||
section.classList.remove('has-open-detail');
|
||
if (backdrop) { backdrop.remove(); backdrop = null; }
|
||
};
|
||
var anim = (!instant && floating.matches && card) ? flyFrom(card, false) : null;
|
||
if (anim) anim.onfinish = done;
|
||
else {
|
||
panel.classList.remove('is-open');
|
||
// Desktop: the card reappears once the panel has folded back onto it.
|
||
if (card) setTimeout(function () { if (source !== card) card.classList.remove('is-source'); }, instant ? 0 : 380);
|
||
section.classList.remove('has-open-detail');
|
||
if (backdrop) { backdrop.remove(); backdrop = null; }
|
||
}
|
||
panel.setAttribute('aria-hidden', 'true');
|
||
source = null;
|
||
touched();
|
||
}
|
||
|
||
// Open: hover (desktop, short intent delay) or click / tap / Enter.
|
||
var hoverTimer = 0;
|
||
scroller.addEventListener('mouseover', function (e) {
|
||
if (floating.matches || dragging) return;
|
||
var card = e.target.closest('.mod-card.has-info');
|
||
if (!card || card === source) return;
|
||
clearTimeout(hoverTimer);
|
||
hoverTimer = setTimeout(function () { if (!dragging) openPanel(card); }, 120);
|
||
});
|
||
scroller.addEventListener('mouseout', function (e) {
|
||
if (floating.matches) return;
|
||
var card = e.target.closest('.mod-card.has-info');
|
||
if (card && !card.contains(e.relatedTarget)) clearTimeout(hoverTimer);
|
||
});
|
||
// The panel sits over the card; leaving the panel closes it.
|
||
panel.addEventListener('mouseleave', function (e) {
|
||
if (floating.matches) return;
|
||
if (source && source.contains(e.relatedTarget)) return;
|
||
closePanel();
|
||
});
|
||
panel.addEventListener('mousemove', function (e) {
|
||
if (floating.matches || !panelOpen()) return;
|
||
var r = panel.getBoundingClientRect();
|
||
var dx = Math.max(-1, Math.min(1, (e.clientX - (r.left + r.width / 2)) / (r.width / 2)));
|
||
var dy = Math.max(-1, Math.min(1, (e.clientY - (r.top + r.height / 2)) / (r.height / 2)));
|
||
panel.style.setProperty('--md-ry', (dx * 5).toFixed(2) + 'deg');
|
||
panel.style.setProperty('--md-rx', (-dy * 5).toFixed(2) + 'deg');
|
||
panel.style.setProperty('--md-mx', (dx * 6).toFixed(1) + 'px');
|
||
panel.style.setProperty('--md-my', (dy * 6 - 6).toFixed(1) + 'px');
|
||
});
|
||
|
||
scroller.addEventListener('click', function (e) {
|
||
var card = e.target.closest('.mod-card.has-info');
|
||
if (!card) return;
|
||
e.stopPropagation();
|
||
openPanel(card);
|
||
});
|
||
scroller.addEventListener('keydown', function (e) {
|
||
var card = e.target.closest('.mod-card.has-info');
|
||
if (card && (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar')) { e.preventDefault(); openPanel(card); }
|
||
});
|
||
panel.addEventListener('click', function (e) {
|
||
e.stopPropagation();
|
||
if (e.target.closest('.md-close')) closePanel();
|
||
});
|
||
document.addEventListener('click', function () { if (panelOpen()) closePanel(); });
|
||
document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && panelOpen()) closePanel(); });
|
||
window.addEventListener('resize', function () { if (panelOpen()) closePanel(true); }, { passive: true });
|
||
window.addEventListener('scroll', function () { if (panelOpen() && !floating.matches && !opening) closePanel(); }, { passive: true });
|
||
});
|
||
|
||
/* ── Schedule day switcher ────────────────────────────────────────────────────
|
||
The on-screen schedule shows the chosen day (phones/tablets) or that day
|
||
centred between the day before and after (desktop ≥992px, .is-3day); each
|
||
header + class cell is tagged with data-day. There is no separate day-picker
|
||
bar — the header itself is the switcher: prev/next arrows beside the day
|
||
name on phones/tablets, and clicking a side day's header on desktop.
|
||
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');
|
||
if (!scope) return;
|
||
var dayheads = scope.querySelectorAll('.schedule_dayhead');
|
||
var cells = scope.querySelectorAll('.schedule_cell, .schedule_dayhead');
|
||
if (!dayheads.length) return;
|
||
var labels = scope.querySelectorAll('.schedule_section_label');
|
||
|
||
// 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(dayheads, function (h) { return h.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) {
|
||
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');
|
||
}
|
||
});
|
||
// 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 = visible.some(function (d) {
|
||
return !!scope.querySelector('.schedule_cell[data-day="' + d + '"][data-section="' + sec + '"]');
|
||
});
|
||
lbl.classList.toggle('sched-hide', !has);
|
||
});
|
||
}
|
||
|
||
function step(dir) {
|
||
var i = week.indexOf(current), n = week.length;
|
||
if (i === -1 || !n) return;
|
||
showDay(week[(i + dir + n) % n]);
|
||
}
|
||
|
||
scope.addEventListener('click', function (e) {
|
||
if (e.target.closest('.sched-prev')) { step(-1); return; }
|
||
if (e.target.closest('.sched-next')) { step(1); return; }
|
||
// Clicking a side day's header (desktop 3-day view) centres it.
|
||
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);
|
||
|
||
// Swipe left/right on phones/tablets to change day — same effect as the
|
||
// arrows. Only acts on mostly-horizontal drags, so a vertical page scroll
|
||
// starting over the schedule is never hijacked.
|
||
var touchStart = null;
|
||
var container = scope.querySelector('.schedule_container') || scope;
|
||
container.addEventListener('touchstart', function (e) {
|
||
var t = e.changedTouches[0];
|
||
touchStart = { x: t.clientX, y: t.clientY };
|
||
}, { passive: true });
|
||
container.addEventListener('touchend', function (e) {
|
||
if (!touchStart) return;
|
||
var t = e.changedTouches[0];
|
||
var dx = t.clientX - touchStart.x, dy = t.clientY - touchStart.y;
|
||
touchStart = null;
|
||
if (Math.abs(dx) < 40 || Math.abs(dx) < Math.abs(dy) * 1.5) return;
|
||
step(dx < 0 ? 1 : -1);
|
||
}, { passive: true });
|
||
|
||
// Pre-select today when the schedule has a header for it (PT day names),
|
||
// otherwise the first day.
|
||
var ptDays = { 1: 'segunda', 2: 'terça', 3: 'quarta', 4: 'quinta', 5: 'sexta', 6: 'sábado' };
|
||
var today = ptDays[new Date().getDay()];
|
||
var start = (today && week.indexOf(today) !== -1) ? today : week[0];
|
||
if (start) showDay(start);
|
||
});
|
||
|
||
/* ── Horário: hide the "Ver Horário das aulas" toggle once open ─────────────
|
||
The button's only job is opening the accordion — once it has, leaving it
|
||
sitting above the now-visible schedule is dead weight (and confusing when
|
||
the navbar link opened it without a click on the button itself). */
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
document.querySelectorAll('.collapse[id^="horarioCollapse"]').forEach(function (el) {
|
||
var toggles = document.querySelectorAll('[data-bs-toggle="collapse"][data-bs-target="#' + el.id + '"]');
|
||
el.addEventListener('shown.bs.collapse', function () {
|
||
toggles.forEach(function (b) { b.hidden = true; });
|
||
});
|
||
el.addEventListener('hidden.bs.collapse', function () {
|
||
toggles.forEach(function (b) { b.hidden = false; });
|
||
});
|
||
});
|
||
});
|
||
|
||
/* ── Equipa: detail card that grows out of the tile ─────────────────────────
|
||
Desktop: hover (or keyboard focus) opens. The card is laid exactly over the
|
||
tile photo, tilted like it, and the CSS unclips/straightens it sideways —
|
||
to the right, or to the left (.td-flip) when there is no room. While open,
|
||
the mouse drives a small 3D tilt + drift so it handles like a real card.
|
||
Touch / narrow screens: tap opens a centred fixed card; a FLIP animation
|
||
makes it fly out of (and back into) the tile photo. Esc, ×, outside tap close. */
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
var items = document.querySelectorAll('.trainer-item.has-detail');
|
||
if (!items.length) return;
|
||
var floating = window.matchMedia('(max-width: 767px), (hover: none)');
|
||
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||
var section = document.getElementById('equipa');
|
||
var CARD_W = 560, GUTTER = 16;
|
||
|
||
function tileRot(item) {
|
||
// #powerRotation tilts odd tiles -3deg and even ones +3deg (style.css).
|
||
var col = item.parentElement, i = 0;
|
||
while (col && (col = col.previousElementSibling)) i++;
|
||
return (i % 2 === 1) ? 3 : -3;
|
||
}
|
||
|
||
// Desktop placement: photo column on top of the tile photo.
|
||
function place(item) {
|
||
var card = item.querySelector('.trainer-detail');
|
||
var pic = item.querySelector('.trainer-pic');
|
||
if (!card || !pic || floating.matches) return;
|
||
var vw = document.documentElement.clientWidth;
|
||
var w = Math.min(CARD_W, vw - GUTTER * 2);
|
||
var pw = pic.offsetWidth, ph = pic.offsetHeight;
|
||
var picLeft = item.getBoundingClientRect().left + pic.offsetLeft; // untransformed
|
||
var flip = picLeft + w > vw - GUTTER; // no room on the right
|
||
var x = flip ? pic.offsetLeft + pw - w : pic.offsetLeft;
|
||
card.classList.toggle('td-flip', flip);
|
||
card.style.setProperty('--td-w', w + 'px');
|
||
card.style.setProperty('--td-pw', pw + 'px');
|
||
card.style.setProperty('--td-ph', ph + 'px');
|
||
card.style.setProperty('--td-x', x + 'px');
|
||
card.style.setProperty('--td-y', pic.offsetTop + 'px');
|
||
card.style.setProperty('--td-rot', tileRot(item) + 'deg');
|
||
// Closed clip = just the photo square, in px (see style.css).
|
||
card.style.setProperty('--td-cx', Math.max(0, w - pw) + 'px');
|
||
card.style.setProperty('--td-cy', Math.max(0, card.offsetHeight - ph) + 'px');
|
||
}
|
||
|
||
function resetTilt(card) {
|
||
['--td-rx', '--td-ry', '--td-mx', '--td-my'].forEach(function (p) { card.style.removeProperty(p); });
|
||
card.classList.remove('td-tracking');
|
||
}
|
||
|
||
// Touch: fly the fixed card out of / back into the tile photo.
|
||
function flip(item, opening) {
|
||
var card = item.querySelector('.trainer-detail');
|
||
var pic = item.querySelector('.trainer-pic');
|
||
if (!card || !pic || !card.animate) return null;
|
||
var p = pic.getBoundingClientRect(), c = card.getBoundingClientRect();
|
||
if (!c.width || !c.height) return null;
|
||
var from = 'translate(' + (p.left - c.left) + 'px,' + (p.top - c.top) + 'px) scale(' +
|
||
(p.width / c.width) + ',' + (p.height / c.height) + ') rotate(' + tileRot(item) + 'deg)';
|
||
var frames = [{ transform: from, opacity: .4 }, { transform: 'none', opacity: 1 }];
|
||
return card.animate(opening ? frames : frames.reverse(), {
|
||
duration: opening ? 420 : 280,
|
||
easing: opening ? 'cubic-bezier(.2,.9,.25,1)' : 'cubic-bezier(.4,0,1,1)'
|
||
});
|
||
}
|
||
|
||
function setOpen(item, open) {
|
||
var card = item.querySelector('.trainer-detail');
|
||
var wasOpen = item.classList.contains('is-open');
|
||
if (open === wasOpen) return;
|
||
clearTimeout(item._tdTrack);
|
||
if (open) {
|
||
if (floating.matches) {
|
||
item.classList.add('is-open');
|
||
card.style.setProperty('--td-top', Math.max(24, (window.innerHeight - card.offsetHeight) / 2) + 'px');
|
||
flip(item, true);
|
||
} else {
|
||
// Jump (no transition) to the closed pose on top of the tile, commit
|
||
// it, then open — otherwise the closed state's own transition would
|
||
// start moving from the previous pose and the opening would begin
|
||
// from that half-way value instead of from the photo.
|
||
card.style.transition = 'none';
|
||
place(item);
|
||
void card.offsetWidth;
|
||
card.style.transition = '';
|
||
void card.offsetWidth;
|
||
item.classList.add('is-open');
|
||
item._tdTrack = setTimeout(function () { card.classList.add('td-tracking'); }, 600);
|
||
}
|
||
} else {
|
||
resetTilt(card);
|
||
var anim = floating.matches ? flip(item, false) : null;
|
||
if (anim) anim.onfinish = function () { item.classList.remove('is-open'); syncSection(); };
|
||
else item.classList.remove('is-open');
|
||
}
|
||
item.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||
syncSection();
|
||
}
|
||
function syncSection() {
|
||
if (section) section.classList.toggle('has-open-detail', !!section.querySelector('.trainer-item.is-open'));
|
||
}
|
||
function closeAll(except) {
|
||
items.forEach(function (it) { if (it !== except) setOpen(it, false); });
|
||
}
|
||
|
||
items.forEach(function (item) {
|
||
var card = item.querySelector('.trainer-detail');
|
||
|
||
// Desktop hover: short intent delay so sweeping across the grid
|
||
// doesn't fire every card.
|
||
item.addEventListener('mouseenter', function () {
|
||
if (floating.matches) return;
|
||
clearTimeout(item._tdHover);
|
||
item._tdHover = setTimeout(function () { closeAll(item); setOpen(item, true); }, 90);
|
||
});
|
||
item.addEventListener('mouseleave', function () {
|
||
if (floating.matches) return;
|
||
clearTimeout(item._tdHover);
|
||
setOpen(item, false);
|
||
});
|
||
|
||
// Tilt + drift toward the pointer while open (desktop only).
|
||
item.addEventListener('mousemove', function (e) {
|
||
if (floating.matches || !item.classList.contains('is-open')) return;
|
||
var r = card.getBoundingClientRect();
|
||
var dx = Math.max(-1, Math.min(1, (e.clientX - (r.left + r.width / 2)) / (r.width / 2)));
|
||
var dy = Math.max(-1, Math.min(1, (e.clientY - (r.top + r.height / 2)) / (r.height / 2)));
|
||
card.style.setProperty('--td-ry', (dx * 5).toFixed(2) + 'deg');
|
||
card.style.setProperty('--td-rx', (-dy * 5).toFixed(2) + 'deg');
|
||
card.style.setProperty('--td-mx', (dx * 6).toFixed(1) + 'px');
|
||
card.style.setProperty('--td-my', (dy * 6).toFixed(1) + 'px');
|
||
});
|
||
|
||
item.addEventListener('focus', function () {
|
||
if (!floating.matches) { closeAll(item); setOpen(item, true); }
|
||
});
|
||
item.addEventListener('blur', function (e) {
|
||
if (!floating.matches && !item.contains(e.relatedTarget)) setOpen(item, false);
|
||
});
|
||
|
||
// Tap / click (touch, or a desktop click on a closed tile).
|
||
item.addEventListener('click', function (e) {
|
||
e.stopPropagation();
|
||
if (e.target.closest('.td-close')) { setOpen(item, false); return; }
|
||
var inCard = !!e.target.closest('.trainer-detail');
|
||
if (item.classList.contains('is-open')) {
|
||
// Tapping the dimmed backdrop (the item's ::after) closes it;
|
||
// taps inside the card (scroll, select) keep it open.
|
||
if (floating.matches && !inCard) setOpen(item, false);
|
||
return;
|
||
}
|
||
closeAll(item);
|
||
setOpen(item, true);
|
||
});
|
||
item.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Escape') { setOpen(item, false); item.blur(); }
|
||
else if ((e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') && e.target === item) {
|
||
e.preventDefault();
|
||
setOpen(item, !item.classList.contains('is-open'));
|
||
}
|
||
});
|
||
});
|
||
document.addEventListener('click', function () { closeAll(null); });
|
||
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeAll(null); });
|
||
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 ───────── */
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
var select = document.querySelector('#joinModal select[name="membership_id"]');
|
||
if (!select) return;
|
||
document.querySelectorAll('[data-bs-target="#joinModal"][data-membership]').forEach(function (btn) {
|
||
btn.addEventListener('click', function () { select.value = btn.getAttribute('data-membership'); });
|
||
});
|
||
});
|
||
|
||
/* ── Galeria: lightbox ───────────────────────────────────────────────────────
|
||
Tiles are plain links to the full image (works without JS). With JS a click
|
||
opens the <dialog> instead: prev / next buttons, ← → keys, Esc, swipe on
|
||
touch, and a click on the dark backdrop closes it. */
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
document.querySelectorAll('.gal-marquee').forEach(function (grid) {
|
||
var box = grid.parentElement.nextElementSibling;
|
||
if (!box || !box.classList.contains('gal-lightbox') || typeof box.showModal !== 'function') return;
|
||
var all = Array.prototype.slice.call(grid.querySelectorAll('.gal-link'));
|
||
var links = all.filter(function (a) { return !a.hasAttribute('data-dup'); }); // the repeated copies open the same photo
|
||
var img = box.querySelector('.gal-lb-figure img');
|
||
var count = box.querySelector('.gal-lb-count');
|
||
var current = 0;
|
||
|
||
function show(i) {
|
||
current = (i + links.length) % links.length;
|
||
var src = links[current].querySelector('img');
|
||
img.src = links[current].getAttribute('href');
|
||
img.alt = src ? src.alt : '';
|
||
// Restart the fade-in for each photo.
|
||
img.style.animation = 'none'; void img.offsetWidth; img.style.animation = '';
|
||
count.textContent = (current + 1) + ' / ' + links.length;
|
||
}
|
||
|
||
all.forEach(function (a) {
|
||
a.addEventListener('click', function (e) {
|
||
e.preventDefault();
|
||
show(+a.getAttribute('data-index') || 0);
|
||
box.showModal();
|
||
});
|
||
});
|
||
box.querySelector('.gal-lb-prev').addEventListener('click', function () { show(current - 1); });
|
||
box.querySelector('.gal-lb-next').addEventListener('click', function () { show(current + 1); });
|
||
box.querySelector('.gal-lb-close').addEventListener('click', function () { box.close(); });
|
||
// Clicks that land on the dialog itself (not the photo or a button) are the backdrop.
|
||
box.addEventListener('click', function (e) { if (e.target === box) box.close(); });
|
||
box.addEventListener('keydown', function (e) {
|
||
if (e.key === 'ArrowLeft') show(current - 1);
|
||
else if (e.key === 'ArrowRight') show(current + 1);
|
||
});
|
||
box.addEventListener('close', function () { if (links[current]) links[current].focus(); });
|
||
|
||
var x0 = null;
|
||
box.addEventListener('touchstart', function (e) { x0 = e.touches[0].clientX; }, { passive: true });
|
||
box.addEventListener('touchend', function (e) {
|
||
if (x0 === null) return;
|
||
var dx = e.changedTouches[0].clientX - x0;
|
||
if (Math.abs(dx) > 40) show(current + (dx < 0 ? 1 : -1));
|
||
x0 = null;
|
||
}, { passive: true });
|
||
});
|
||
});
|
||
|
||
/* ── 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
|
||
of the viewport (±12% of its height); inside a sliding strip (gallery,
|
||
Modalidades) they must also cross the horizontal middle. Throttled, paused
|
||
while the tab is hidden, skipped with "reduce motion". The CSS mirrors the
|
||
:hover rules (style.css, "In the middle of the screen"). */
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
if (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||
var items = Array.prototype.slice.call(document.querySelectorAll(
|
||
'.gal-tile, .trainer-item .trainer-pic, .icon-box-item, .price-card')).map(function (el) {
|
||
return { el: el, strip: !!el.closest('.gal-marquee, .mod-scroll'), on: false };
|
||
});
|
||
if (!items.length) return;
|
||
var last = 0;
|
||
function tick(now) {
|
||
requestAnimationFrame(tick);
|
||
if (document.hidden || now - last < 120) return;
|
||
last = now;
|
||
var vw = window.innerWidth, vh = window.innerHeight, cy = vh / 2, band = vh * 0.12;
|
||
items.forEach(function (it) {
|
||
var r = it.el.getBoundingClientRect();
|
||
var on = r.bottom > 0 && r.top < vh &&
|
||
r.top <= cy + band && r.bottom >= cy - band &&
|
||
(!it.strip || (r.left <= vw / 2 && r.right >= vw / 2));
|
||
if (on !== it.on) { it.on = on; it.el.classList.toggle('is-centered', on); }
|
||
});
|
||
}
|
||
requestAnimationFrame(tick);
|
||
});
|