/** * Page Transitions and Smooth Navigation * Handles page loading, transitions, and history management */ class PageTransitions { constructor() { this.transitionDuration = 300; this.isTransitioning = false; this.init(); } init() { // Wait for body to exist if (!document.body) return; // Add transition wrapper if it doesn't exist if (!document.getElementById("page-transition")) { const wrapper = document.createElement("div"); wrapper.id = "page-transition"; wrapper.className = "page-transition"; // Wrap main content const main = document.querySelector("main") || document.body; const parent = main.parentNode; parent.insertBefore(wrapper, main); wrapper.appendChild(main); } // Add fade-in on page load this.fadeIn(); // Intercept navigation clicks this.setupLinkInterception(); // Handle back/forward buttons window.addEventListener("popstate", (e) => { if (e.state && e.state.url) { this.navigate(e.state.url, false); } }); // Add scroll restoration if ("scrollRestoration" in history) { history.scrollRestoration = "manual"; } } fadeIn() { const wrapper = document.getElementById("page-transition"); if (wrapper) { wrapper.classList.add("fade-in"); setTimeout(() => { wrapper.classList.remove("fade-in"); }, this.transitionDuration); } } fadeOut(callback) { const wrapper = document.getElementById("page-transition"); if (wrapper) { wrapper.classList.add("fade-out"); setTimeout(() => { if (callback) callback(); wrapper.classList.remove("fade-out"); }, this.transitionDuration); } else { if (callback) callback(); } } setupLinkInterception() { document.addEventListener("click", (e) => { const link = e.target.closest("a"); // Check if it's a valid internal link if (!link) return; if (link.hasAttribute("data-no-transition")) return; if (link.target === "_blank") return; if (link.hasAttribute("download")) return; const href = link.getAttribute("href"); if ( !href || href.startsWith("#") || href.startsWith("mailto:") || href.startsWith("tel:") ) return; // Check if it's an external link const url = new URL(href, window.location.origin); if (url.origin !== window.location.origin) return; // Intercept the navigation e.preventDefault(); this.navigate(href, true); }); } navigate(url, updateHistory = true) { if (this.isTransitioning) return; this.isTransitioning = true; this.fadeOut(() => { if (updateHistory) { history.pushState({ url }, "", url); } window.location.href = url; }); } // Scroll to element with smooth animation scrollTo(selector, offset = 0) { const element = document.querySelector(selector); if (!element) return; const top = element.getBoundingClientRect().top + window.pageYOffset - offset; window.scrollTo({ top, behavior: "smooth", }); } // Scroll to top scrollToTop() { window.scrollTo({ top: 0, behavior: "smooth", }); } } /** * Lazy Loading Images * Improves performance by loading images only when they're visible */ class LazyLoader { constructor() { this.images = []; this.observer = null; this.init(); } init() { // Find all lazy images this.images = document.querySelectorAll( 'img[data-src], img[loading="lazy"]' ); // Set up Intersection Observer if ("IntersectionObserver" in window) { this.observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { this.loadImage(entry.target); } }); }, { rootMargin: "50px", } ); this.images.forEach((img) => this.observer.observe(img)); } else { // Fallback for older browsers this.images.forEach((img) => this.loadImage(img)); } } loadImage(img) { const src = img.getAttribute("data-src"); if (src) { img.src = src; img.removeAttribute("data-src"); } // Add fade-in effect img.addEventListener("load", () => { img.classList.add("loaded"); }); if (this.observer) { this.observer.unobserve(img); } } // Add new images to observer observe(images) { if (!images) return; const imageList = Array.isArray(images) ? images : [images]; imageList.forEach((img) => { if (this.observer) { this.observer.observe(img); } else { this.loadImage(img); } }); } } /** * Smooth Scroll Handler * Adds smooth scrolling to anchor links */ class SmoothScroll { constructor() { this.init(); } init() { document.querySelectorAll('a[href^="#"]').forEach((anchor) => { anchor.addEventListener("click", (e) => { const href = anchor.getAttribute("href"); if (href === "#") return; e.preventDefault(); const target = document.querySelector(href); if (target) { const offset = 80; // Account for fixed header const top = target.getBoundingClientRect().top + window.pageYOffset - offset; window.scrollTo({ top, behavior: "smooth", }); // Update URL without scrolling history.pushState(null, "", href); } }); }); } } /** * Back to Top Button * Shows/hides button based on scroll position */ class BackToTop { constructor() { this.button = null; this.scrollThreshold = 300; this.init(); } init() { // Wait for body to exist if (!document.body) return; // Create button if it doesn't exist this.button = document.getElementById("back-to-top"); if (!this.button) { this.button = document.createElement("button"); this.button.id = "back-to-top"; this.button.className = "back-to-top"; this.button.innerHTML = "↑"; this.button.setAttribute("aria-label", "Back to top"); document.body.appendChild(this.button); } // Handle scroll window.addEventListener("scroll", () => { if (window.pageYOffset > this.scrollThreshold) { this.button.classList.add("visible"); } else { this.button.classList.remove("visible"); } }); // Handle click this.button.addEventListener("click", () => { window.scrollTo({ top: 0, behavior: "smooth", }); }); } } /** * Loading Overlay * Shows loading state during async operations */ class LoadingOverlay { constructor() { this.overlay = null; this.activeOperations = 0; this.init(); } init() { // Wait for body to exist if (!document.body) return; // Create overlay if it doesn't exist this.overlay = document.getElementById("loading-overlay"); if (!this.overlay) { this.overlay = document.createElement("div"); this.overlay.id = "loading-overlay"; this.overlay.className = "loading-overlay"; this.overlay.innerHTML = `
Loading...