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(); }