update design
This commit is contained in:
10 files changed
+1266
-835
No files matched your search
@@ -1,16 +1,17 @@
|
||||
const NAV_OFFSET = 120;
|
||||
const DEFAULT_VISIBLE_PROJECTS = 9;
|
||||
const INTERACTIVE_SELECTOR =
|
||||
'a, button, input, textarea, select, summary, [role="button"]';
|
||||
|
||||
initRevealOnScroll();
|
||||
initActiveNavLink();
|
||||
initProjectCardLinks();
|
||||
initProjectFilters();
|
||||
initMobileNav();
|
||||
initActiveNavLink();
|
||||
initRevealOnScroll();
|
||||
|
||||
function initRevealOnScroll() {
|
||||
const revealTargets = document.querySelectorAll('.reveal');
|
||||
if (revealTargets.length === 0) {
|
||||
if (
|
||||
revealTargets.length === 0 ||
|
||||
!('IntersectionObserver' in window) ||
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -28,91 +29,79 @@ function initRevealOnScroll() {
|
||||
{ threshold: 0.1, rootMargin: '0px 0px -40px 0px' }
|
||||
);
|
||||
|
||||
revealTargets.forEach((target) => observer.observe(target));
|
||||
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'));
|
||||
if (sections.length === 0 || navLinks.length === 0) {
|
||||
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 updateActiveNavLink = () => {
|
||||
let currentSectionId = '';
|
||||
|
||||
sections.forEach((section) => {
|
||||
if (window.scrollY >= section.offsetTop - NAV_OFFSET) {
|
||||
currentSectionId = section.id;
|
||||
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) => {
|
||||
link.classList.toggle(
|
||||
'is-active',
|
||||
link.getAttribute('href') === `#${currentSectionId}`
|
||||
);
|
||||
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');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', updateActiveNavLink, { passive: true });
|
||||
updateActiveNavLink();
|
||||
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 initProjectCardLinks() {
|
||||
const cardLinks = document.querySelectorAll('.card-link[data-href]');
|
||||
if (cardLinks.length === 0) {
|
||||
function initMobileNav() {
|
||||
const menu = document.querySelector('.nav-menu');
|
||||
if (!menu) {
|
||||
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);
|
||||
menu.querySelectorAll('a').forEach((link) => {
|
||||
link.addEventListener('click', () => {
|
||||
menu.open = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
menu.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape' && menu.open) {
|
||||
menu.open = false;
|
||||
menu.querySelector('summary').focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initProjectFilters() {
|
||||
@@ -125,6 +114,11 @@ function initProjectFilters() {
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -136,36 +130,47 @@ function initProjectFilters() {
|
||||
let isExpanded = false;
|
||||
|
||||
const applyFilter = () => {
|
||||
let visibleCount = 0;
|
||||
|
||||
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) => {
|
||||
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;
|
||||
card.hidden = !shownSet.has(card);
|
||||
});
|
||||
|
||||
if (!seeMoreButton) {
|
||||
return;
|
||||
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'}`;
|
||||
}
|
||||
|
||||
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) => {
|
||||
@@ -173,10 +178,6 @@ function initProjectFilters() {
|
||||
selectedFilter = chip.dataset.filter || 'all';
|
||||
isExpanded = false;
|
||||
|
||||
filterChips.forEach((otherChip) => {
|
||||
otherChip.classList.toggle('active', otherChip === chip);
|
||||
});
|
||||
|
||||
applyFilter();
|
||||
});
|
||||
});
|
||||
@@ -185,8 +186,23 @@ function initProjectFilters() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user