2025-05-03 02:27:26 -07:00
|
|
|
---
|
2025-05-03 14:06:52 -07:00
|
|
|
import LoadingIndicator from "./LoadingIndicator.astro";
|
|
|
|
|
2025-05-03 02:27:26 -07:00
|
|
|
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">
|
2025-05-03 14:06:52 -07:00
|
|
|
<!-- Enhanced loading indicator -->
|
|
|
|
<div class="mb-6 flex justify-center">
|
|
|
|
<LoadingIndicator type="dots" size="large" color="var(--color-zag-accent-dark)" />
|
2025-05-03 02:27:26 -07:00
|
|
|
</div>
|
|
|
|
|
2025-05-03 14:06:52 -07:00
|
|
|
<!-- 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>
|
2025-05-03 02:27:26 -07:00
|
|
|
</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;
|
|
|
|
}
|
|
|
|
|
2025-05-03 14:06:52 -07:00
|
|
|
/* Message animations */
|
|
|
|
.message-animate {
|
|
|
|
animation: fadeSlideUp 0.6s ease-out both;
|
2025-05-03 02:27:26 -07:00
|
|
|
}
|
|
|
|
|
2025-05-03 14:06:52 -07:00
|
|
|
.message-animate-delay {
|
|
|
|
animation: fadeSlideUp 0.6s ease-out 0.2s both;
|
2025-05-03 02:27:26 -07:00
|
|
|
}
|
|
|
|
|
2025-05-03 14:06:52 -07:00
|
|
|
@keyframes fadeSlideUp {
|
|
|
|
from {
|
|
|
|
opacity: 0;
|
|
|
|
transform: translateY(10px);
|
2025-05-03 02:27:26 -07:00
|
|
|
}
|
2025-05-03 14:06:52 -07:00
|
|
|
to {
|
|
|
|
opacity: 1;
|
|
|
|
transform: translateY(0);
|
2025-05-03 02:27:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@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>
|