update design

This commit is contained in:
John Gatward committed 2026-10-08 11:59:24 +01:00
1 parent a8fde951a6
commit 874ea1f8bf
10 files changed
+1266 -835

No files matched your search

+120 -104
View File
@@ -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;
}
}