Files
havox/script.js
John Gatward 415b76532a tidy up
2026-03-31 21:56:26 +01:00

175 lines
5.0 KiB
JavaScript

const NAV_OFFSET = 120;
const DEFAULT_VISIBLE_PROJECTS = 9;
const INTERACTIVE_SELECTOR = 'a, button, input, textarea, select, summary, [role="button"]';
initRevealOnScroll();
initActiveNavLink();
initProjectCardLinks();
initProjectFilters();
function initRevealOnScroll() {
const revealTargets = document.querySelectorAll('.reveal');
if (revealTargets.length === 0) {
return;
}
const observer = new IntersectionObserver((entries, currentObserver) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
}
entry.target.classList.add('visible');
currentObserver.unobserve(entry.target);
});
}, {threshold: 0.1, rootMargin: '0px 0px -40px 0px'});
revealTargets.forEach((target) => observer.observe(target));
}
function initActiveNavLink() {
const sections = Array.from(document.querySelectorAll('section[id]'));
const navLinks = Array.from(document.querySelectorAll('.nav-links a'));
if (sections.length === 0 || navLinks.length === 0) {
return;
}
const updateActiveNavLink = () => {
let currentSectionId = '';
sections.forEach((section) => {
if (window.scrollY >= section.offsetTop - NAV_OFFSET) {
currentSectionId = section.id;
}
});
navLinks.forEach((link) => {
link.classList.toggle('is-active', link.getAttribute('href') === `#${currentSectionId}`);
});
};
window.addEventListener('scroll', updateActiveNavLink, {passive: true});
updateActiveNavLink();
}
function initProjectCardLinks() {
const cardLinks = document.querySelectorAll('.card-link[data-href]');
if (cardLinks.length === 0) {
return;
}
cardLinks.forEach((card) => {
card.setAttribute('role', 'link');
card.setAttribute('tabindex', '0');
card.addEventListener('click', (event) => {
if (shouldIgnoreCardActivation(card, event.target)) {
return;
}
navigateToCard(card, event);
});
card.addEventListener('keydown', (event) => {
if (event.key !== 'Enter' && event.key !== ' ') {
return;
}
if (shouldIgnoreCardActivation(card, event.target)) {
return;
}
event.preventDefault();
navigateToCard(card, event);
});
});
}
function shouldIgnoreCardActivation(card, target) {
if (!(target instanceof Element)) {
return false;
}
const interactiveAncestor = target.closest(INTERACTIVE_SELECTOR);
return interactiveAncestor !== null && interactiveAncestor !== card;
}
function navigateToCard(card, event) {
const href = card.dataset.href;
if (!href) {
return;
}
if (event.metaKey || event.ctrlKey || event.button === 1) {
window.open(href, '_blank', 'noopener');
return;
}
window.location.assign(href);
}
function initProjectFilters() {
const filterChips = Array.from(document.querySelectorAll('.filter-chip'));
const projectCards = Array.from(document.querySelectorAll('#project-grid .project-card'));
if (filterChips.length === 0 || projectCards.length === 0) {
return;
}
const seeMoreButton = document.querySelector('#projects-see-more');
const techByCard = new Map(
projectCards.map((card) => [card, (card.dataset.tech || '').split(/\s+/).filter(Boolean)]),
);
let selectedFilter = 'all';
let isExpanded = false;
const applyFilter = () => {
let visibleCount = 0;
projectCards.forEach((card) => {
const tech = techByCard.get(card) || [];
const matchesFilter = selectedFilter === 'all' || tech.includes(selectedFilter);
card.classList.toggle('is-hidden', !matchesFilter);
if (!matchesFilter) {
card.classList.remove('is-collapsed');
return;
}
const isCollapsed = !isExpanded && visibleCount >= DEFAULT_VISIBLE_PROJECTS;
card.classList.toggle('is-collapsed', isCollapsed);
visibleCount += 1;
});
if (!seeMoreButton) {
return;
}
const canExpand = visibleCount > DEFAULT_VISIBLE_PROJECTS;
seeMoreButton.hidden = !canExpand;
seeMoreButton.setAttribute('aria-expanded', canExpand && isExpanded ? 'true' : 'false');
seeMoreButton.textContent = isExpanded ? 'See less' : 'See more';
};
filterChips.forEach((chip) => {
chip.addEventListener('click', () => {
selectedFilter = chip.dataset.filter || 'all';
isExpanded = false;
filterChips.forEach((otherChip) => {
otherChip.classList.toggle('active', otherChip === chip);
});
applyFilter();
});
});
if (seeMoreButton) {
seeMoreButton.addEventListener('click', () => {
isExpanded = !isExpanded;
applyFilter();
});
}
applyFilter();
}