← back to Jill Website

public/js/adaptive-loading.js

365 lines

/**
 * Adaptive Loading Manager
 * Manages adaptive loading of images and videos based on connection speed
 */

class AdaptiveLoadingManager {
    constructor() {
        this.detector = window.connectionSpeed;
        this.loadingImages = new Set();
        this.loadedImages = new Set();
        this.observers = new Map();

        this.init();
    }

    /**
     * Initialize adaptive loading
     */
    init() {
        // Wait for DOM to be ready
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => this.setupAdaptiveLoading());
        } else {
            this.setupAdaptiveLoading();
        }

        // Listen for connection changes
        window.addEventListener('connectionchange', (event) => {
            this.handleConnectionChange(event.detail);
        });
    }

    /**
     * Setup adaptive loading for all images and videos
     */
    setupAdaptiveLoading() {
        console.log('Setting up adaptive loading with speed:', this.detector.speedCategory);

        // Process all images
        this.processImages();

        // Process all videos
        this.processVideos();

        // Setup intersection observer for lazy loading
        this.setupIntersectionObserver();
    }

    /**
     * Process all images on the page
     */
    processImages() {
        const images = document.querySelectorAll('img[data-adaptive]');

        images.forEach(img => {
            this.applyAdaptiveImage(img);
        });

        // Also handle regular images that should be adaptive
        const regularImages = document.querySelectorAll('.villa-image img, .hero img, .gallery-item img');
        regularImages.forEach(img => {
            if (!img.hasAttribute('data-adaptive')) {
                this.makeImageAdaptive(img);
            }
        });
    }

    /**
     * Make a regular image adaptive
     */
    makeImageAdaptive(img) {
        const originalSrc = img.getAttribute('src');
        if (!originalSrc || originalSrc.startsWith('data:')) return;

        img.setAttribute('data-src-high', originalSrc);
        img.setAttribute('data-adaptive', 'true');

        this.applyAdaptiveImage(img);
    }

    /**
     * Apply adaptive loading to an image
     */
    applyAdaptiveImage(img) {
        const quality = this.detector.getImageQuality();
        const highSrc = img.getAttribute('data-src-high') || img.getAttribute('src');
        const mediumSrc = img.getAttribute('data-src-medium') || highSrc;
        const lowSrc = img.getAttribute('data-src-low') || highSrc;

        // Show loading indicator
        this.showLoadingIndicator(img);

        let targetSrc = highSrc;

        switch (quality) {
            case 'low':
                targetSrc = lowSrc;
                break;
            case 'medium':
                targetSrc = mediumSrc;
                break;
            case 'high':
                targetSrc = highSrc;
                break;
        }

        // Load image with loading state
        this.loadImage(img, targetSrc);
    }

    /**
     * Load an image with progress tracking
     */
    loadImage(img, src) {
        if (this.loadedImages.has(img)) return;

        this.loadingImages.add(img);

        const tempImg = new Image();

        tempImg.onload = () => {
            img.src = src;
            this.loadingImages.delete(img);
            this.loadedImages.add(img);
            this.hideLoadingIndicator(img);
            img.classList.add('adaptive-loaded');
        };

        tempImg.onerror = () => {
            console.error('Failed to load image:', src);
            this.loadingImages.delete(img);
            this.hideLoadingIndicator(img);
            img.classList.add('adaptive-error');
        };

        tempImg.src = src;
    }

    /**
     * Process all videos on the page
     */
    processVideos() {
        const videos = document.querySelectorAll('video, iframe[src*="youtube"], iframe[src*="vimeo"]');

        videos.forEach(video => {
            this.applyAdaptiveVideo(video);
        });
    }

    /**
     * Apply adaptive loading to a video
     */
    applyAdaptiveVideo(video) {
        const shouldEnable = this.detector.shouldEnableVideos();
        const quality = this.detector.getVideoQuality();

        if (!shouldEnable || quality === 'disabled') {
            // Disable video and show placeholder
            this.disableVideo(video);
        } else {
            // Enable video with appropriate quality
            this.enableVideo(video, quality);
        }
    }

    /**
     * Disable video and show placeholder
     */
    disableVideo(video) {
        // Create placeholder
        const placeholder = document.createElement('div');
        placeholder.className = 'video-placeholder';
        placeholder.innerHTML = `
            <div class="video-placeholder-content">
                <i class="fas fa-video-slash"></i>
                <p>Video disabled for slow connection</p>
                <button class="btn-secondary video-enable-btn">Load Video Anyway</button>
            </div>
        `;

        // Replace video with placeholder
        video.style.display = 'none';
        video.parentNode.insertBefore(placeholder, video);

        // Add click handler to enable video
        const enableBtn = placeholder.querySelector('.video-enable-btn');
        enableBtn.addEventListener('click', () => {
            placeholder.remove();
            video.style.display = '';
            this.enableVideo(video, 'medium');
        });
    }

    /**
     * Enable video with specified quality
     */
    enableVideo(video, quality) {
        if (video.tagName === 'VIDEO') {
            // Native video element
            if (!video.hasAttribute('data-original-autoplay')) {
                video.setAttribute('data-original-autoplay', video.autoplay.toString());
            }

            // Disable autoplay for medium quality
            if (quality === '480p' || quality === 'medium') {
                video.autoplay = false;
                video.preload = 'metadata';
            } else {
                video.preload = 'auto';
            }
        } else if (video.tagName === 'IFRAME') {
            // YouTube or Vimeo iframe
            const src = video.getAttribute('src');
            if (src && !src.includes('autoplay=0')) {
                // Disable autoplay for medium quality
                if (quality === '480p' || quality === 'medium') {
                    const newSrc = src.includes('?') ? `${src}&autoplay=0` : `${src}?autoplay=0`;
                    video.setAttribute('src', newSrc);
                }
            }
        }

        video.classList.add('adaptive-video-enabled');
    }

    /**
     * Show loading indicator for an element
     */
    showLoadingIndicator(element) {
        const indicator = document.createElement('div');
        indicator.className = 'adaptive-loading-indicator';
        indicator.innerHTML = `
            <div class="spinner"></div>
            <span>Loading...</span>
        `;

        element.classList.add('adaptive-loading');

        // Position indicator
        const parent = element.parentElement;
        if (parent && !parent.querySelector('.adaptive-loading-indicator')) {
            parent.style.position = 'relative';
            parent.appendChild(indicator);
        }
    }

    /**
     * Hide loading indicator for an element
     */
    hideLoadingIndicator(element) {
        element.classList.remove('adaptive-loading');

        const parent = element.parentElement;
        if (parent) {
            const indicator = parent.querySelector('.adaptive-loading-indicator');
            if (indicator) {
                indicator.remove();
            }
        }
    }

    /**
     * Setup intersection observer for lazy loading
     */
    setupIntersectionObserver() {
        if (!('IntersectionObserver' in window)) {
            // Fallback: Load all images immediately
            this.processImages();
            return;
        }

        const observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    const img = entry.target;
                    if (img.hasAttribute('data-adaptive') && !this.loadedImages.has(img)) {
                        this.applyAdaptiveImage(img);
                    }
                    observer.unobserve(img);
                }
            });
        }, {
            rootMargin: '50px'
        });

        // Observe all adaptive images
        document.querySelectorAll('img[data-adaptive]').forEach(img => {
            observer.observe(img);
        });

        this.observers.set('images', observer);
    }

    /**
     * Handle connection speed change
     */
    handleConnectionChange(detail) {
        console.log('Connection changed:', detail);

        // Show notification
        this.showConnectionNotification(detail.speedCategory);

        // Optionally reload content with new quality
        // (commented out to avoid disrupting user experience)
        // this.setupAdaptiveLoading();
    }

    /**
     * Show connection speed notification
     */
    showConnectionNotification(speedCategory) {
        const notification = document.createElement('div');
        notification.className = 'connection-notification';

        let message = '';
        switch (speedCategory) {
            case 'slow':
                message = 'Slow connection detected. Loading optimized content.';
                break;
            case 'medium':
                message = 'Medium connection detected. Loading balanced content.';
                break;
            case 'fast':
                message = 'Fast connection detected. Loading full quality content.';
                break;
        }

        notification.innerHTML = `
            <i class="fas fa-wifi"></i>
            <span>${message}</span>
        `;

        document.body.appendChild(notification);

        // Auto-hide after 3 seconds
        setTimeout(() => {
            notification.classList.add('fade-out');
            setTimeout(() => notification.remove(), 500);
        }, 3000);
    }

    /**
     * Get loading status
     */
    getStatus() {
        return {
            loading: this.loadingImages.size,
            loaded: this.loadedImages.size,
            speedCategory: this.detector.speedCategory
        };
    }
}

// Initialize on page load
window.AdaptiveLoadingManager = AdaptiveLoadingManager;

// Auto-initialize
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => {
        window.adaptiveLoading = new AdaptiveLoadingManager();
    });
} else {
    window.adaptiveLoading = new AdaptiveLoadingManager();
}