const DEFAULT_VISIBLE_PROJECTS = 9; initProjectFilters(); initMobileNav(); initActiveNavLink(); initRevealOnScroll(); function initRevealOnScroll() { const revealTargets = document.querySelectorAll('.reveal'); if ( revealTargets.length === 0 || !('IntersectionObserver' in window) || window.matchMedia('(prefers-reduced-motion: reduce)').matches ) { 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); target.classList.add('reveal-pending'); }); } function initActiveNavLink() { const sections = Array.from(document.querySelectorAll('section[id]')); const navLinks = Array.from( document.querySelectorAll('.nav-links a, .mobile-nav-links a') ); if ( sections.length === 0 || navLinks.length === 0 || !('IntersectionObserver' in window) ) { return; } const visibleSections = new Set(); const updateActiveNavLink = (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { visibleSections.add(entry.target); } else { visibleSections.delete(entry.target); } }); const currentSection = sections .filter((section) => visibleSections.has(section)) .at(-1); if (!currentSection) { return; } navLinks.forEach((link) => { const isActive = link.getAttribute('href') === `#${currentSection.id}`; link.classList.toggle('is-active', isActive); if (isActive) { link.setAttribute('aria-current', 'location'); } else { link.removeAttribute('aria-current'); } }); }; let observer; const observeSections = () => { observer?.disconnect(); visibleSections.clear(); observer = new IntersectionObserver(updateActiveNavLink, { rootMargin: `-56px 0px -${Math.max(0, window.innerHeight - 160)}px 0px`, }); sections.forEach((section) => observer.observe(section)); }; observeSections(); window.addEventListener('resize', observeSections); } function initMobileNav() { const menu = document.querySelector('.nav-menu'); if (!menu) { return; } menu.querySelectorAll('a').forEach((link) => { link.addEventListener('click', () => { menu.open = false; }); }); menu.addEventListener('keydown', (event) => { if (event.key === 'Escape' && menu.open) { menu.open = false; menu.querySelector('summary').focus(); } }); } 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 projectCount = document.querySelector('#project-count'); const emptyState = document.querySelector('#projects-empty'); const projectGrid = document.querySelector('#project-grid'); const filters = document.querySelector('.project-filters'); const resetButton = document.querySelector('#projects-reset'); const techByCard = new Map( projectCards.map((card) => [ card, (card.dataset.tech || '').split(/\s+/).filter(Boolean), ]) ); let selectedFilter = 'all'; let isExpanded = false; const applyFilter = () => { const matchingCards = projectCards.filter( (card) => selectedFilter === 'all' || techByCard.get(card).includes(selectedFilter) ); const shownCards = isExpanded ? matchingCards : matchingCards.slice(0, DEFAULT_VISIBLE_PROJECTS); const shownSet = new Set(shownCards); projectCards.forEach((card) => { card.hidden = !shownSet.has(card); }); filterChips.forEach((chip) => { const isSelected = chip.dataset.filter === selectedFilter; chip.classList.toggle('active', isSelected); chip.setAttribute('aria-pressed', String(isSelected)); }); if (projectCount) { const label = filterChips .find((chip) => chip.dataset.filter === selectedFilter) .textContent.trim(); projectCount.hidden = false; projectCount.textContent = `Showing ${shownCards.length} of ${matchingCards.length} project${matchingCards.length === 1 ? '' : 's'}` + (selectedFilter === 'all' ? '' : ` (${label})`); } if (emptyState) { emptyState.hidden = matchingCards.length !== 0; } if (projectGrid) { projectGrid.hidden = matchingCards.length === 0; } if (seeMoreButton) { const remainingCount = matchingCards.length - shownCards.length; seeMoreButton.hidden = matchingCards.length <= DEFAULT_VISIBLE_PROJECTS; seeMoreButton.setAttribute('aria-expanded', String(isExpanded)); seeMoreButton.textContent = isExpanded ? 'Show fewer projects' : `Show ${remainingCount} more project${remainingCount === 1 ? '' : 's'}`; } }; filterChips.forEach((chip) => { chip.addEventListener('click', () => { selectedFilter = chip.dataset.filter || 'all'; isExpanded = false; applyFilter(); }); }); if (seeMoreButton) { seeMoreButton.addEventListener('click', () => { isExpanded = !isExpanded; applyFilter(); if (!isExpanded) { seeMoreButton.scrollIntoView({ block: 'nearest', behavior: 'instant' }); } }); } if (resetButton) { resetButton.addEventListener('click', () => { selectedFilter = 'all'; isExpanded = false; applyFilter(); filterChips.find((chip) => chip.dataset.filter === 'all').focus(); }); } applyFilter(); if (filters) { filters.hidden = false; } }