209 lines
5.9 KiB
JavaScript
209 lines
5.9 KiB
JavaScript
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;
|
|
}
|
|
}
|