Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 33 additions & 11 deletions src/assets/js/components/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
* Projects section with modern design
*/

// Module-level observer to prevent memory leaks
let projectObserver = null;

export const populateProjects = (projects) => {
const projectsContainer = document.getElementById('projects-container');
if (!projectsContainer) return;
Expand Down Expand Up @@ -29,9 +32,9 @@ export const populateProjects = (projects) => {
${
project.image
? `
<img
src="${project.image}"
alt="${project.title}"
<img
src="${project.image}"
alt="${project.title}"
class="card-img"
loading="lazy"
>
Expand Down Expand Up @@ -65,10 +68,10 @@ export const populateProjects = (projects) => {
${
project.link
? `
<a
href="${project.link}"
class="btn btn-primary"
target="_blank"
<a
href="${project.link}"
class="btn btn-primary"
target="_blank"
rel="noopener noreferrer"
aria-label="View ${project.title}"
>
Expand All @@ -81,8 +84,8 @@ export const populateProjects = (projects) => {
${
project.github
? `
<a
href="${project.github}"
<a
href="${project.github}"
class="btn btn-outline btn-sm border-gray-300 text-gray-700 bg-white hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:bg-gray-800 dark:hover:bg-gray-700"
target="_blank"
rel="noopener noreferrer"
Expand All @@ -109,14 +112,22 @@ export const populateProjects = (projects) => {

// Add intersection observer for scroll animations
const animateOnScroll = () => {
// Cleanup previous observer to prevent memory leaks
if (projectObserver) {
projectObserver.disconnect();
projectObserver = null;
}

const elements = document.querySelectorAll('.fade-in-up');

const observer = new IntersectionObserver(
projectObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
// Unobserve after animation to free memory
projectObserver.unobserve(entry.target);
}
});
},
Expand All @@ -127,7 +138,7 @@ export const populateProjects = (projects) => {
el.style.opacity = '0';
el.style.transform = 'translateY(20px)';
el.style.transition = 'opacity 0.6s ease-out, transform 0.6s ease-out';
observer.observe(el);
projectObserver.observe(el);
});
};

Expand All @@ -138,3 +149,14 @@ export const populateProjects = (projects) => {
animateOnScroll();
}
};

/**
* Cleanup function for SPA scenarios or component unmounting
* Call this to properly clean up the IntersectionObserver
*/
export const cleanupProjects = () => {
if (projectObserver) {
projectObserver.disconnect();
projectObserver = null;
}
};
75 changes: 53 additions & 22 deletions src/assets/js/utils/mobileMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,66 @@
* Mobile menu functionality
*/

// Store cleanup function to prevent memory leaks
let cleanupMobileMenu = null;

export const initMobileMenu = () => {
const mobileMenuButton = document.getElementById('mobile-menu-button');
const mobileMenu = document.getElementById('mobile-menu');

if (mobileMenuButton && mobileMenu) {
mobileMenuButton.addEventListener('click', () => {
mobileMenu.classList.toggle('hidden');
});
if (!mobileMenuButton || !mobileMenu) return;

// Close mobile menu when clicking outside
document.addEventListener('click', (event) => {
const isClickInsideMenu = mobileMenu.contains(event.target);
const isClickOnButton = mobileMenuButton.contains(event.target);

if (
!isClickInsideMenu &&
!isClickOnButton &&
!mobileMenu.classList.contains('hidden')
) {
mobileMenu.classList.add('hidden');
}
});
// Cleanup previous listeners if reinitializing
if (cleanupMobileMenu) {
cleanupMobileMenu();
}

// Define event handlers so they can be removed later
const handleMenuToggle = () => {
mobileMenu.classList.toggle('hidden');
};

const handleOutsideClick = (event) => {
const isClickInsideMenu = mobileMenu.contains(event.target);
const isClickOnButton = mobileMenuButton.contains(event.target);

if (!isClickInsideMenu && !isClickOnButton && !mobileMenu.classList.contains('hidden')) {
mobileMenu.classList.add('hidden');
}
};

// Close mobile menu when a navigation link is clicked
const mobileNavLinks = mobileMenu.querySelectorAll('a');
// Close mobile menu when a navigation link is clicked
const mobileNavLinks = mobileMenu.querySelectorAll('a');
const handleLinkClick = () => {
mobileMenu.classList.add('hidden');
};

// Attach event listeners
mobileMenuButton.addEventListener('click', handleMenuToggle);
document.addEventListener('click', handleOutsideClick);
mobileNavLinks.forEach((link) => {
link.addEventListener('click', handleLinkClick);
});

// Store cleanup function to remove all event listeners
cleanupMobileMenu = () => {
mobileMenuButton.removeEventListener('click', handleMenuToggle);
document.removeEventListener('click', handleOutsideClick);
mobileNavLinks.forEach((link) => {
link.addEventListener('click', () => {
mobileMenu.classList.add('hidden');
});
link.removeEventListener('click', handleLinkClick);
});
cleanupMobileMenu = null;
};

return cleanupMobileMenu;
};

/**
* Cleanup function to remove mobile menu event listeners
* Call this when the mobile menu is no longer needed (e.g., SPA navigation)
*/
export const cleanup = () => {
if (cleanupMobileMenu) {
cleanupMobileMenu();
}
};