/** * 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...

`; document.body.appendChild(this.overlay); } } show() { this.activeOperations++; this.overlay.classList.add("active"); document.body.style.overflow = "hidden"; } hide() { this.activeOperations = Math.max(0, this.activeOperations - 1); if (this.activeOperations === 0) { this.overlay.classList.remove("active"); document.body.style.overflow = ""; } } // Force hide regardless of operation count forceHide() { this.activeOperations = 0; this.overlay.classList.remove("active"); document.body.style.overflow = ""; } } /** * Page Visibility Handler * Handles actions when page becomes visible/hidden */ class PageVisibility { constructor() { this.callbacks = { visible: [], hidden: [], }; this.init(); } init() { document.addEventListener("visibilitychange", () => { if (document.hidden) { this.callbacks.hidden.forEach((cb) => cb()); } else { this.callbacks.visible.forEach((cb) => cb()); } }); } onVisible(callback) { this.callbacks.visible.push(callback); } onHidden(callback) { this.callbacks.hidden.push(callback); } } /** * Network Status Handler * Monitors online/offline status */ class NetworkStatus { constructor() { this.isOnline = navigator.onLine; this.callbacks = { online: [], offline: [], }; this.init(); } init() { window.addEventListener("online", () => { this.isOnline = true; this.callbacks.online.forEach((cb) => cb()); this.showNotification("Back online", "success"); }); window.addEventListener("offline", () => { this.isOnline = false; this.callbacks.offline.forEach((cb) => cb()); this.showNotification("No internet connection", "error"); }); } onOnline(callback) { this.callbacks.online.push(callback); } onOffline(callback) { this.callbacks.offline.push(callback); } showNotification(message, type) { if (window.Utils && window.Utils.notify) { window.Utils.notify(message, type); } } } // Initialize when DOM is ready if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initPageTransitions); } else { initPageTransitions(); } function initPageTransitions() { // Initialize all modules window.pageTransitions = new PageTransitions(); window.lazyLoader = new LazyLoader(); window.smoothScroll = new SmoothScroll(); window.backToTop = new BackToTop(); window.loadingOverlay = new LoadingOverlay(); window.pageVisibility = new PageVisibility(); window.networkStatus = new NetworkStatus(); console.log("Page transitions initialized"); } // Add CSS if not already present if (!document.getElementById("page-transitions-styles")) { const style = document.createElement("style"); style.id = "page-transitions-styles"; style.textContent = ` .page-transition { opacity: 1; transition: opacity 300ms ease; } .page-transition.fade-in { opacity: 0; animation: fadeIn 300ms ease forwards; } .page-transition.fade-out { opacity: 1; animation: fadeOut 300ms ease forwards; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } img[data-src] { opacity: 0; transition: opacity 300ms ease; } img.loaded { opacity: 1; } .back-to-top { position: fixed; bottom: 20px; right: 20px; width: 50px; height: 50px; background: #667eea; color: white; border: none; border-radius: 50%; font-size: 24px; cursor: pointer; opacity: 0; visibility: hidden; transform: translateY(20px); transition: all 0.3s ease; z-index: 999; box-shadow: 0 4px 12px rgba(0,0,0,0.2); } .back-to-top.visible { opacity: 1; visibility: visible; transform: translateY(0); } .back-to-top:hover { background: #5568d3; transform: translateY(-2px); } .loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.95); display: flex; align-items: center; justify-content: center; opacity: 0; visibility: hidden; transition: all 0.3s ease; z-index: 9999; } .loading-overlay.active { opacity: 1; visibility: visible; } .loading-spinner { text-align: center; } .spinner { width: 60px; height: 60px; border: 4px solid #f3f3f3; border-top: 4px solid #667eea; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 16px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .loading-spinner p { color: #667eea; font-size: 16px; font-weight: 600; margin: 0; } `; document.head.appendChild(style); }