From 415b76532a77073240158dd37d2f9beb3fd33a58 Mon Sep 17 00:00:00 2001 From: John Gatward Date: Tue, 31 Mar 2026 21:56:26 +0100 Subject: [PATCH] tidy up --- script.js | 201 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 111 insertions(+), 90 deletions(-) diff --git a/script.js b/script.js index 8c1ab0a..3962cb5 100644 --- a/script.js +++ b/script.js @@ -1,130 +1,151 @@ -// ─── Scroll reveal ───────────────────────────────────────── -const reveals = document.querySelectorAll('.reveal'); -const revealObserver = new IntersectionObserver((entries, observer) => { - entries.forEach((entry) => { - if (!entry.isIntersecting) { - return; - } - - entry.target.classList.add('visible'); - observer.unobserve(entry.target); - }); -}, {threshold: 0.1, rootMargin: '0px 0px -40px 0px'}); -reveals.forEach((element) => revealObserver.observe(element)); - -// ─── Active nav link on scroll ────────────────────────────── -const sections = Array.from(document.querySelectorAll('section[id]')); -const navLinks = Array.from(document.querySelectorAll('.nav-links a')); -const updateActiveNavLink = () => { - let currentSectionId = ''; - - sections.forEach((section) => { - if (window.scrollY >= section.offsetTop - 120) { - currentSectionId = section.id; - } - }); - - navLinks.forEach((link) => { - const isActive = link.getAttribute('href') === `#${currentSectionId}`; - link.classList.toggle('is-active', isActive); - }); -}; -window.addEventListener('scroll', updateActiveNavLink, {passive: true}); -updateActiveNavLink(); - -// ─── Project card links + filters ─────────────────────────── -const filterChips = Array.from(document.querySelectorAll('.filter-chip')); -const projectCards = Array.from(document.querySelectorAll('#project-grid .project-card')); -const seeMoreButton = document.querySelector('#projects-see-more'); -const cardLinks = document.querySelectorAll('.card-link[data-href]'); +const NAV_OFFSET = 120; const DEFAULT_VISIBLE_PROJECTS = 9; -const interactiveSelector = 'a, button, input, textarea, select, summary, [role="button"]'; +const INTERACTIVE_SELECTOR = 'a, button, input, textarea, select, summary, [role="button"]'; -const isNestedInteractiveTarget = (card, target) => { +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(interactiveSelector); + const interactiveAncestor = target.closest(INTERACTIVE_SELECTOR); return interactiveAncestor !== null && interactiveAncestor !== card; -}; +} -const goToCardHref = (card, event) => { +function navigateToCard(card, event) { const href = card.dataset.href; if (!href) { return; } - const openInNewTab = event.metaKey || event.ctrlKey || event.button === 1; - if (openInNewTab) { + if (event.metaKey || event.ctrlKey || event.button === 1) { window.open(href, '_blank', 'noopener'); return; } - globalThis.location.href = href; -}; + window.location.assign(href); +} -cardLinks.forEach((card) => { - card.setAttribute('role', 'link'); - card.setAttribute('tabindex', '0'); - - card.addEventListener('click', (event) => { - if (isNestedInteractiveTarget(card, event.target)) { - return; - } - - goToCardHref(card, event); - }); - - card.addEventListener('keydown', (event) => { - if (event.key !== 'Enter' && event.key !== ' ') { - return; - } - - if (isNestedInteractiveTarget(card, event.target)) { - return; - } - - event.preventDefault(); - goToCardHref(card, event); - }); -}); - -if (filterChips.length > 0 && projectCards.length > 0) { - let selectedFilter = 'all'; - let isExpanded = false; +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 = () => { - const visibleCards = []; + let visibleCount = 0; projectCards.forEach((card) => { const tech = techByCard.get(card) || []; - const shouldShow = selectedFilter === 'all' || tech.includes(selectedFilter); - card.classList.toggle('is-hidden', !shouldShow); + const matchesFilter = selectedFilter === 'all' || tech.includes(selectedFilter); - if (!shouldShow) { + card.classList.toggle('is-hidden', !matchesFilter); + if (!matchesFilter) { + card.classList.remove('is-collapsed'); return; } - card.classList.remove('is-collapsed'); - visibleCards.push(card); + const isCollapsed = !isExpanded && visibleCount >= DEFAULT_VISIBLE_PROJECTS; + card.classList.toggle('is-collapsed', isCollapsed); + visibleCount += 1; }); - if (!isExpanded) { - visibleCards.slice(DEFAULT_VISIBLE_PROJECTS).forEach((card) => { - card.classList.add('is-collapsed'); - }); - } - if (!seeMoreButton) { return; } - const canExpand = visibleCards.length > DEFAULT_VISIBLE_PROJECTS; + const canExpand = visibleCount > DEFAULT_VISIBLE_PROJECTS; seeMoreButton.hidden = !canExpand; seeMoreButton.setAttribute('aria-expanded', canExpand && isExpanded ? 'true' : 'false'); seeMoreButton.textContent = isExpanded ? 'See less' : 'See more';