// ─── Scroll reveal ───────────────────────────────────────── const reveals = document.querySelectorAll('.reveal'); const revealObserver = new IntersectionObserver((entries, observer) => { entries.forEach((entry) => { if (!entry.isIntersecting) { return; } entry.target.classList.add('visible'); observer.unobserve(entry.target); }); }, {threshold: 0.1, rootMargin: '0px 0px -40px 0px'}); reveals.forEach((element) => revealObserver.observe(element)); // ─── Active nav link on scroll ────────────────────────────── const sections = Array.from(document.querySelectorAll('section[id]')); const navLinks = Array.from(document.querySelectorAll('.nav-links a')); const updateActiveNavLink = () => { let currentSectionId = ''; sections.forEach((section) => { if (window.scrollY >= section.offsetTop - 120) { currentSectionId = section.id; } }); navLinks.forEach((link) => { const isActive = link.getAttribute('href') === `#${currentSectionId}`; link.classList.toggle('is-active', isActive); }); }; window.addEventListener('scroll', updateActiveNavLink, {passive: true}); updateActiveNavLink(); // ─── Project card links + filters ─────────────────────────── const filterChips = Array.from(document.querySelectorAll('.filter-chip')); const projectCards = Array.from(document.querySelectorAll('#project-grid .project-card')); const seeMoreButton = document.querySelector('#projects-see-more'); const cardLinks = document.querySelectorAll('.card-link[data-href]'); const DEFAULT_VISIBLE_PROJECTS = 9; const interactiveSelector = 'a, button, input, textarea, select, summary, [role="button"]'; const isNestedInteractiveTarget = (card, target) => { if (!(target instanceof Element)) { return false; } const interactiveAncestor = target.closest(interactiveSelector); return interactiveAncestor !== null && interactiveAncestor !== card; }; const goToCardHref = (card, event) => { const href = card.dataset.href; if (!href) { return; } const openInNewTab = event.metaKey || event.ctrlKey || event.button === 1; if (openInNewTab) { window.open(href, '_blank', 'noopener'); return; } globalThis.location.href = href; }; cardLinks.forEach((card) => { card.setAttribute('role', 'link'); card.setAttribute('tabindex', '0'); card.addEventListener('click', (event) => { if (isNestedInteractiveTarget(card, event.target)) { return; } goToCardHref(card, event); }); card.addEventListener('keydown', (event) => { if (event.key !== 'Enter' && event.key !== ' ') { return; } if (isNestedInteractiveTarget(card, event.target)) { return; } event.preventDefault(); goToCardHref(card, event); }); }); if (filterChips.length > 0 && projectCards.length > 0) { let selectedFilter = 'all'; let isExpanded = false; const techByCard = new Map( projectCards.map((card) => [card, (card.dataset.tech || '').split(/\s+/).filter(Boolean)]), ); const applyFilter = () => { const visibleCards = []; projectCards.forEach((card) => { const tech = techByCard.get(card) || []; const shouldShow = selectedFilter === 'all' || tech.includes(selectedFilter); card.classList.toggle('is-hidden', !shouldShow); if (!shouldShow) { return; } card.classList.remove('is-collapsed'); visibleCards.push(card); }); if (!isExpanded) { visibleCards.slice(DEFAULT_VISIBLE_PROJECTS).forEach((card) => { card.classList.add('is-collapsed'); }); } if (!seeMoreButton) { return; } const canExpand = visibleCards.length > 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(); }