Files
spore-website/public/script.js
2025-10-14 13:53:30 +02:00

170 lines
5.5 KiB
JavaScript

// Smooth scroll for navigation links
function getHeaderHeight() {
const header = document.querySelector('header');
return header ? header.offsetHeight : 80;
}
function smoothScrollToSection(hash) {
const target = document.querySelector(hash);
if (!target) return;
// Recalculate header height at the moment of scrolling
const headerHeight = getHeaderHeight();
// Use getBoundingClientRect for robust position calc across layouts
const targetTopRelative = target.getBoundingClientRect().top;
const absoluteTargetTop = window.scrollY + targetTopRelative;
// Exact offset: align section start just below the header bottom
const top = Math.max(absoluteTargetTop - headerHeight, 0);
window.scrollTo({ top, behavior: 'smooth' });
}
function updateURLHash(hash) {
// Update URL hash without triggering scroll
if (hash && hash !== '#') {
history.pushState(null, null, hash);
}
}
function scrollToTop() {
// Remove hash from URL and scroll to top
history.pushState(null, null, window.location.pathname);
window.scrollTo({ top: 0, behavior: 'smooth' });
}
document.querySelectorAll('nav a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
// If mobile menu is open, close it first so header height is correct
const siteNavEl = document.getElementById('site-nav');
const menuBtn = document.getElementById('menu-toggle');
const wasOpen = siteNavEl && siteNavEl.classList.contains('open');
if (wasOpen) {
siteNavEl.classList.remove('open');
if (menuBtn) menuBtn.setAttribute('aria-expanded', 'false');
// Wait for next frame so layout updates and header height is correct
requestAnimationFrame(() => {
smoothScrollToSection(this.getAttribute('href'));
updateURLHash(this.getAttribute('href'));
});
} else {
smoothScrollToSection(this.getAttribute('href'));
updateURLHash(this.getAttribute('href'));
}
});
});
// Add active class to navigation based on scroll position
const sections = document.querySelectorAll('section[id]');
const navLinks = document.querySelectorAll('nav a');
function setActiveNav() {
let current = '';
const headerHeight = getHeaderHeight();
sections.forEach(section => {
const sectionTop = section.offsetTop;
const sectionHeight = section.clientHeight;
// Account for fixed header in active link detection
if (window.scrollY + headerHeight + 30 >= sectionTop && window.scrollY < sectionTop + sectionHeight) {
current = section.getAttribute('id');
}
});
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === `#${current}`) {
link.classList.add('active');
}
});
}
window.addEventListener('scroll', setActiveNav);
window.addEventListener('load', setActiveNav);
// Adjust on resize (header height can change)
window.addEventListener('resize', () => {
// Re-run active link calculation to keep alignment correct
setActiveNav();
});
// Handle direct hash navigation (e.g., page load with #section)
window.addEventListener('load', () => {
if (location.hash) {
// Use the same precise scroll logic on initial load
smoothScrollToSection(location.hash);
}
});
// Lightbox for screenshots (desktop only behavior)
const lightbox = document.getElementById('lightbox');
const lightboxImg = document.getElementById('lightbox-image');
const lightboxCaption = document.getElementById('lightbox-caption');
function openLightbox(src, caption) {
if (!lightbox) return;
lightboxImg.src = src;
lightboxCaption.textContent = caption || '';
lightbox.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
}
function closeLightbox() {
if (!lightbox) return;
lightbox.setAttribute('aria-hidden', 'true');
lightboxImg.src = '';
document.body.style.overflow = '';
}
// Delegate clicks on screenshots
document.addEventListener('click', (e) => {
const img = e.target.closest('.screenshot img');
if (img) {
// Only open overlay on wider screens
if (window.innerWidth >= 768) {
openLightbox(img.src, img.alt || img.nextElementSibling?.textContent || '');
e.preventDefault();
}
}
if (e.target.matches('[data-close]')) {
closeLightbox();
}
});
// Close on ESC
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeLightbox();
});
// Mobile burger menu
const menuToggle = document.getElementById('menu-toggle');
const siteNav = document.getElementById('site-nav');
if (menuToggle && siteNav) {
menuToggle.addEventListener('click', () => {
const isOpen = siteNav.classList.toggle('open');
menuToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
// Close menu after navigating
siteNav.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
if (siteNav.classList.contains('open')) {
siteNav.classList.remove('open');
menuToggle.setAttribute('aria-expanded', 'false');
}
});
});
}
// Handle SPORE title click to scroll to top and remove hash
const sporeTitle = document.querySelector('.brand h1');
if (sporeTitle) {
sporeTitle.addEventListener('click', scrollToTop);
sporeTitle.style.cursor = 'pointer';
}