tidy up
This commit is contained in:
201
script.js
201
script.js
@@ -1,130 +1,151 @@
|
|||||||
// ─── Scroll reveal ─────────────────────────────────────────
|
const NAV_OFFSET = 120;
|
||||||
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 DEFAULT_VISIBLE_PROJECTS = 9;
|
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)) {
|
if (!(target instanceof Element)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const interactiveAncestor = target.closest(interactiveSelector);
|
const interactiveAncestor = target.closest(INTERACTIVE_SELECTOR);
|
||||||
return interactiveAncestor !== null && interactiveAncestor !== card;
|
return interactiveAncestor !== null && interactiveAncestor !== card;
|
||||||
};
|
}
|
||||||
|
|
||||||
const goToCardHref = (card, event) => {
|
function navigateToCard(card, event) {
|
||||||
const href = card.dataset.href;
|
const href = card.dataset.href;
|
||||||
if (!href) {
|
if (!href) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const openInNewTab = event.metaKey || event.ctrlKey || event.button === 1;
|
if (event.metaKey || event.ctrlKey || event.button === 1) {
|
||||||
if (openInNewTab) {
|
|
||||||
window.open(href, '_blank', 'noopener');
|
window.open(href, '_blank', 'noopener');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
globalThis.location.href = href;
|
window.location.assign(href);
|
||||||
};
|
}
|
||||||
|
|
||||||
cardLinks.forEach((card) => {
|
function initProjectFilters() {
|
||||||
card.setAttribute('role', 'link');
|
const filterChips = Array.from(document.querySelectorAll('.filter-chip'));
|
||||||
card.setAttribute('tabindex', '0');
|
const projectCards = Array.from(document.querySelectorAll('#project-grid .project-card'));
|
||||||
|
if (filterChips.length === 0 || projectCards.length === 0) {
|
||||||
card.addEventListener('click', (event) => {
|
return;
|
||||||
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;
|
|
||||||
|
|
||||||
|
const seeMoreButton = document.querySelector('#projects-see-more');
|
||||||
const techByCard = new Map(
|
const techByCard = new Map(
|
||||||
projectCards.map((card) => [card, (card.dataset.tech || '').split(/\s+/).filter(Boolean)]),
|
projectCards.map((card) => [card, (card.dataset.tech || '').split(/\s+/).filter(Boolean)]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let selectedFilter = 'all';
|
||||||
|
let isExpanded = false;
|
||||||
|
|
||||||
const applyFilter = () => {
|
const applyFilter = () => {
|
||||||
const visibleCards = [];
|
let visibleCount = 0;
|
||||||
|
|
||||||
projectCards.forEach((card) => {
|
projectCards.forEach((card) => {
|
||||||
const tech = techByCard.get(card) || [];
|
const tech = techByCard.get(card) || [];
|
||||||
const shouldShow = selectedFilter === 'all' || tech.includes(selectedFilter);
|
const matchesFilter = selectedFilter === 'all' || tech.includes(selectedFilter);
|
||||||
card.classList.toggle('is-hidden', !shouldShow);
|
|
||||||
|
|
||||||
if (!shouldShow) {
|
card.classList.toggle('is-hidden', !matchesFilter);
|
||||||
|
if (!matchesFilter) {
|
||||||
|
card.classList.remove('is-collapsed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
card.classList.remove('is-collapsed');
|
const isCollapsed = !isExpanded && visibleCount >= DEFAULT_VISIBLE_PROJECTS;
|
||||||
visibleCards.push(card);
|
card.classList.toggle('is-collapsed', isCollapsed);
|
||||||
|
visibleCount += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isExpanded) {
|
|
||||||
visibleCards.slice(DEFAULT_VISIBLE_PROJECTS).forEach((card) => {
|
|
||||||
card.classList.add('is-collapsed');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!seeMoreButton) {
|
if (!seeMoreButton) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const canExpand = visibleCards.length > DEFAULT_VISIBLE_PROJECTS;
|
const canExpand = visibleCount > DEFAULT_VISIBLE_PROJECTS;
|
||||||
seeMoreButton.hidden = !canExpand;
|
seeMoreButton.hidden = !canExpand;
|
||||||
seeMoreButton.setAttribute('aria-expanded', canExpand && isExpanded ? 'true' : 'false');
|
seeMoreButton.setAttribute('aria-expanded', canExpand && isExpanded ? 'true' : 'false');
|
||||||
seeMoreButton.textContent = isExpanded ? 'See less' : 'See more';
|
seeMoreButton.textContent = isExpanded ? 'See less' : 'See more';
|
||||||
|
|||||||
Reference in New Issue
Block a user