Added open source links

This commit is contained in:
John Gatward
2026-03-31 21:54:01 +01:00
parent 13c8b0a28e
commit 16bd425c20
7 changed files with 1348 additions and 1208 deletions

195
script.js
View File

@@ -1,89 +1,142 @@
// ─── Scroll reveal ─────────────────────────────────────────
const reveals = document.querySelectorAll('.reveal');
const observer = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
e.target.classList.add('visible');
observer.unobserve(e.target);
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(el => observer.observe(el));
reveals.forEach((element) => revealObserver.observe(element));
// ─── Active nav link on scroll ──────────────────────────────
const sections = document.querySelectorAll('section[id]');
const navLinks = document.querySelectorAll('.nav-links a');
window.addEventListener('scroll', () => {
let current = '';
sections.forEach(s => {
if (window.scrollY >= s.offsetTop - 120) current = s.id;
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(a => {
a.style.color = a.getAttribute('href') === `#${current}` ? 'var(--mauve)' : '';
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 DEFAULT_VISIBLE_PROJECTS = 9;
const interactiveSelector = 'a, button, input, textarea, select, summary, [role="button"]';
const isNestedInteractiveTarget = (card, target) => {
if (!(target instanceof Element)) {
return false;
}
const interactiveAncestor = target.closest(interactiveSelector);
return interactiveAncestor !== null && interactiveAncestor !== card;
};
const goToCardHref = (card, event) => {
const href = card.dataset.href;
if (!href) {
return;
}
const openInNewTab = event.metaKey || event.ctrlKey || event.button === 1;
if (openInNewTab) {
window.open(href, '_blank', 'noopener');
return;
}
globalThis.location.href = 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);
});
});
// ─── Project filters ──────────────────────────────────────────────
const filterChips = document.querySelectorAll('.filter-chip');
const projectCards = document.querySelectorAll('#project-grid .project-card');
const seeMoreButton = document.querySelector('#projects-see-more');
const DEFAULT_VISIBLE_PROJECTS = 9;
if (filterChips.length > 0 && projectCards.length > 0) {
let selectedFilter = 'all';
let isExpanded = false;
const interactiveSelector = 'a, button, input, textarea, select, summary, [role="button"]';
const goToCardHref = (card, event) => {
const href = card.dataset.href;
if (!href) {
return;
}
const openInNewTab = event.metaKey || event.ctrlKey || event.button === 1;
if (openInNewTab) {
window.open(href, '_blank', 'noopener');
return;
}
globalThis.location.href = href;
};
const techByCard = new Map(
projectCards.map((card) => [card, (card.dataset.tech || '').split(/\s+/).filter(Boolean)]),
);
const applyFilter = () => {
const visibleCards = [];
projectCards.forEach(card => {
const tech = (card.dataset.tech || '').split(/\s+/).filter(Boolean);
projectCards.forEach((card) => {
const tech = techByCard.get(card) || [];
const shouldShow = selectedFilter === 'all' || tech.includes(selectedFilter);
card.classList.toggle('is-hidden', !shouldShow);
if (shouldShow) {
card.classList.remove('is-collapsed');
visibleCards.push(card);
if (!shouldShow) {
return;
}
card.classList.remove('is-collapsed');
visibleCards.push(card);
});
if (!isExpanded) {
visibleCards.slice(DEFAULT_VISIBLE_PROJECTS).forEach(card => {
visibleCards.slice(DEFAULT_VISIBLE_PROJECTS).forEach((card) => {
card.classList.add('is-collapsed');
});
}
if (seeMoreButton) {
const canExpand = visibleCards.length > DEFAULT_VISIBLE_PROJECTS;
seeMoreButton.hidden = !canExpand;
seeMoreButton.setAttribute('aria-expanded', canExpand && isExpanded ? 'true' : 'false');
seeMoreButton.textContent = isExpanded ? 'See less' : 'See more';
if (!seeMoreButton) {
return;
}
const canExpand = visibleCards.length > DEFAULT_VISIBLE_PROJECTS;
seeMoreButton.hidden = !canExpand;
seeMoreButton.setAttribute('aria-expanded', canExpand && isExpanded ? 'true' : 'false');
seeMoreButton.textContent = isExpanded ? 'See less' : 'See more';
};
filterChips.forEach(chip => {
filterChips.forEach((chip) => {
chip.addEventListener('click', () => {
selectedFilter = chip.dataset.filter || 'all';
isExpanded = false;
filterChips.forEach(other => {
other.classList.toggle('active', other === chip);
filterChips.forEach((otherChip) => {
otherChip.classList.toggle('active', otherChip === chip);
});
applyFilter();
@@ -97,47 +150,5 @@ if (filterChips.length > 0 && projectCards.length > 0) {
});
}
projectCards.forEach(card => {
if (!card.dataset.href) {
return;
}
card.setAttribute('role', 'link');
card.setAttribute('tabindex', '0');
card.addEventListener('click', event => {
const target = event.target;
if (!(target instanceof Element)) {
return;
}
// Let inline links and other controls inside the card handle their own clicks.
if (target.closest(interactiveSelector) && target.closest(interactiveSelector) !== card) {
return;
}
goToCardHref(card, event);
});
card.addEventListener('keydown', event => {
if (event.key !== 'Enter' && event.key !== ' ') {
return;
}
const target = event.target;
if (!(target instanceof Element)) {
return;
}
if (target.closest(interactiveSelector) && target.closest(interactiveSelector) !== card) {
return;
}
event.preventDefault();
goToCardHref(card, event);
});
});
applyFilter();
}
}