justin.deal/src/components/common/LoadingOverlay.astro

225 lines
6.2 KiB
Plaintext
Raw Normal View History

---
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">
<!-- Spinner -->
<div class="spinner-container mb-6 flex justify-center">
<div class="spinner"></div>
</div>
<!-- Messages -->
<p class="text-xl font-semibold mb-2 zag-text">{message}</p>
<p class="text-base zag-text-muted">{subMessage}</p>
</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;
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid var(--color-zag-dark-muted);
border-radius: 50%;
border-top-color: var(--color-zag-accent-dark);
animation: spin 1.5s cubic-bezier(0.68, -0.55, 0.27, 1.55) infinite;
}
:global(.dark) .spinner {
border-color: var(--color-zag-light-muted);
border-top-color: var(--color-zag-accent-dark);
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@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>