--- import LoadingIndicator from "./LoadingIndicator.astro"; interface Props { message?: string; subMessage?: string; className?: string; } const { message = "This is taking longer than expected...", subMessage = "Still working on loading your content", className = "" } = Astro.props; --- <div class={`loading-overlay fixed inset-0 flex flex-col items-center justify-center z-50 bg-opacity-90 zag-bg ${className}`} role="alert" aria-live="polite" > <div class="loading-content text-center p-8 rounded-lg"> <!-- Enhanced loading indicator --> <div class="mb-6 flex justify-center"> <LoadingIndicator type="dots" size="large" color="var(--color-zag-accent-dark)" /> </div> <!-- Messages with animation --> <div class="messages-container"> <p class="text-xl font-semibold mb-2 zag-text message-animate">{message}</p> <p class="text-base zag-text-muted message-animate-delay">{subMessage}</p> </div> </div> </div> <style> .loading-overlay { opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0.3s ease; } .loading-overlay.visible { opacity: 1; visibility: visible; } /* Message animations */ .message-animate { animation: fadeSlideUp 0.6s ease-out both; } .message-animate-delay { animation: fadeSlideUp 0.6s ease-out 0.2s both; } @keyframes fadeSlideUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } @media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 3s; } } </style> <script> // This script will be executed client-side class LoadingManager { overlay: HTMLElement | null = null; timeoutId: ReturnType<typeof setTimeout> | null = null; longLoadingTimeoutId: ReturnType<typeof setTimeout> | null = null; constructor() { this.overlay = document.querySelector('.loading-overlay'); this.setupNavigationListeners(); } setupNavigationListeners() { // Listen for navigation events if the Navigation API is supported if ('navigation' in window) { // @ts-ignore - Navigation API might not be recognized by TypeScript window.navigation.addEventListener('navigate', (event) => { // Only handle same-origin navigations if (new URL(event.destination.url).origin === window.location.origin) { this.showLoading(); } }); // @ts-ignore window.navigation.addEventListener('navigatesuccess', () => { this.hideLoading(); }); // @ts-ignore window.navigation.addEventListener('navigateerror', () => { this.hideLoading(); }); } else { // Fallback for browsers without Navigation API window.addEventListener('beforeunload', () => { this.showLoading(); }); // For SPA navigation (if using client-side routing) document.addEventListener('astro:page-load', () => { this.hideLoading(); }); } // For Astro's view transitions document.addEventListener('astro:before-swap', () => { this.showLoading(); }); document.addEventListener('astro:after-swap', () => { this.hideLoading(); }); } showLoading() { // Clear any existing timeouts if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } if (this.longLoadingTimeoutId) { clearTimeout(this.longLoadingTimeoutId); this.longLoadingTimeoutId = null; } // Check connection speed if available const showDelay = this.getConnectionBasedDelay(); // Only show loading UI after a short delay to avoid flashing on fast loads this.timeoutId = setTimeout(() => { // Set loading state in Alpine.js components if they exist document.querySelectorAll('[x-data]').forEach(el => { // @ts-ignore if (el.__x && typeof el.__x.$data.loading !== 'undefined') { // @ts-ignore el.__x.$data.loading = true; } }); }, showDelay); // Show the overlay for long loading times (5+ seconds) this.longLoadingTimeoutId = setTimeout(() => { if (this.overlay) { this.overlay.classList.add('visible'); } }, 5000); } hideLoading() { // Clear timeouts if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } if (this.longLoadingTimeoutId) { clearTimeout(this.longLoadingTimeoutId); this.longLoadingTimeoutId = null; } // Hide the overlay if (this.overlay) { this.overlay.classList.remove('visible'); } // Reset loading state in Alpine.js components after a short delay // This ensures transitions look smooth setTimeout(() => { document.querySelectorAll('[x-data]').forEach(el => { // @ts-ignore if (el.__x && typeof el.__x.$data.loading !== 'undefined') { // @ts-ignore el.__x.$data.loading = false; } }); }, 100); } getConnectionBasedDelay() { // Use Network Information API if available // @ts-ignore - Navigator connection API might not be recognized const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; if (connection) { const type = connection.effectiveType; // Adjust delay based on connection type switch (type) { case '4g': return 100; // Fast connection - short delay case '3g': return 200; // Medium connection case '2g': case 'slow-2g': return 0; // Slow connection - show immediately default: return 100; // Default delay } } // Default delay if Network Information API is not available return 100; } } // Initialize the loading manager when the DOM is ready document.addEventListener('DOMContentLoaded', () => { new LoadingManager(); }); </script>