Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ruby displays duplicate posts in infinite scroll - have an update, but main.js in Ruby is somewhat terse #384

Open
mheland opened this issue Jan 19, 2025 · 0 comments

Comments

@mheland
Copy link

mheland commented Jan 19, 2025

Running Ruby on a installation with 400+ posts, and get duplicates in the infinite scroll.
I have an update to main.js which resolves it.
Ruby's main.js looks like this in the repo, I'm a little lost on how to create a branch here..

Image

Here is my main.js which uses a Set() to track post ID and prevent duplicates, feel free to merge it.

(() => {
    // Track loaded post IDs to prevent duplicates
    const loadedPosts = new Set();

    // Initialize pagination
    pagination(true);

    // Handle mobile menu
    const burger = document.querySelector('.gh-burger');
    if (burger) {
        burger.addEventListener('click', function () {
            if (document.body.classList.contains('is-head-open')) {
                document.body.classList.remove('is-head-open');
            } else {
                document.body.classList.add('is-head-open');
            }
        });
    }

    // Initialize lightbox for images
    lightbox('.kg-image-card > .kg-image[width][height], .kg-gallery-image > img');

    // Handle responsive iframes
    reframe(document.querySelectorAll([
        '.gh-content iframe[src*="youtube.com"]',
        '.gh-content iframe[src*="youtube-nocookie.com"]',
        '.gh-content iframe[src*="player.vimeo.com"]',
        '.gh-content iframe[src*="kickstarter.com"][src*="video.html"]',
        '.gh-content object',
        '.gh-content embed'
    ].join(',')));

    // Initialize dropdown
    dropdown();

    /**
     * Enhanced pagination function that handles both infinite scroll and load-more button functionality
     * 
     * @param {boolean} isInitial - If true, sets up infinite scroll using IntersectionObserver.
     *                              If false, uses load-more button functionality.
     * @param {Function} callback - Optional callback function executed after loading new posts.
     *                             Receives (uniquePosts, nextElement) as parameters.
     * @param {boolean} waitForImages - If true, new posts are initially hidden until images load.
     *                                  Helps prevent layout shifts during image loading.
     */
    function pagination(isInitial, callback, waitForImages = false) {
        // Main container for posts
        const container = document.querySelector('.gh-feed');
        if (!container) return;

        // State tracking for preventing concurrent loads
        let isLoading = false;

        // Element used to trigger infinite scroll
        // Falls back through multiple options to find a suitable trigger element
        let nextElement = container.nextElementSibling || container.parentElement.nextElementSibling || document.querySelector('.gh-foot');
        
        // Load more button for manual pagination if infinite scroll is disabled
        const loadMoreButton = document.querySelector('.gh-loadmore');

        // Early cleanup: Remove load more button if there are no more pages to load
        if (!document.querySelector('link[rel=next]')) {
            loadMoreButton?.remove();
            return;
        }

        /**
         * Fetches and appends posts from the next page
         * - Fetches HTML from next page URL
         * - Parses response and extracts posts
         * - Filters out duplicate posts using URL tracking
         * - Appends unique posts to container
         * - Updates pagination links
         * - Handles visibility for image loading
         */
        async function loadNextPage() {
            const nextPageUrl = document.querySelector('link[rel=next]');
            if (!nextPageUrl) return;

            try {
                const response = await fetch(nextPageUrl.href);
                const html = await response.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(html, 'text/html');
                
                // Extract all posts from next page, excluding featured and related posts
                const newPosts = Array.from(doc.querySelectorAll('.gh-feed:not(.gh-featured):not(.gh-related) > *'));
                
                // Remove any posts that have already been loaded to prevent duplicates
                // Uses post URLs stored in loadedPosts Set for tracking
                const uniquePosts = newPosts.filter(post => {
                    const postLink = post.querySelector('a.post-link');
                    if (!postLink) return false;
                    
                    const postUrl = postLink.href;
                    if (loadedPosts.has(postUrl)) {
                        return false;
                    }
                    
                    loadedPosts.add(postUrl);
                    return true;
                });

                // Use DocumentFragment for better performance when adding multiple posts
                // Optionally hide posts initially if waiting for images to load
                const fragment = document.createDocumentFragment();
                uniquePosts.forEach(post => {
                    const importedPost = document.importNode(post, true);
                    if (waitForImages) {
                        importedPost.style.visibility = 'hidden';
                    }
                    fragment.appendChild(importedPost);
                });
                
                container.appendChild(fragment);

                // Update or remove pagination links based on next page availability
                // Removes load more button when reaching the last page
                const newNextLink = doc.querySelector('link[rel=next]');
                if (newNextLink && newNextLink.href) {
                    nextPageUrl.href = newNextLink.href;
                } else {
                    nextPageUrl.remove();
                    loadMoreButton?.remove();
                }

                // Execute callback with newly loaded posts and next element reference
                if (callback) {
                    callback(uniquePosts, nextElement);
                }

            } catch (error) {
                console.error('Error loading next page:', error);
                nextPageUrl.remove();
                loadMoreButton?.remove();
            }
        }

        /**
         * Handles infinite scroll functionality
         * - Checks if more pages are available
         * - Prevents concurrent loading
         * - Triggers load when next element comes into view
         */
        async function handleScroll() {
            if (!document.querySelector('link[rel=next]')) return;
            
            if (isLoading) return;
            
            if (nextElement.getBoundingClientRect().top <= window.innerHeight) {
                isLoading = true;
                await loadNextPage();
                isLoading = false;
            }
        }

        /**
         * Initialize pagination based on mode (infinite scroll vs load more button)
         * For infinite scroll (isInitial = true):
         * - Sets up IntersectionObserver to detect when more content should load
         * - Handles both immediate and delayed loading based on waitForImages
         * For load more button (isInitial = false):
         * - Attaches click handler to load more button
         */
        if (isInitial) {
            const observer = new IntersectionObserver(async function(entries) {
                if (isLoading) return;
                
                if (entries[0].isIntersecting) {
                    isLoading = true;
                    if (waitForImages) {
                        await loadNextPage();
                    } else {
                        while (nextElement.getBoundingClientRect().top <= window.innerHeight && document.querySelector('link[rel=next]')) {
                            await loadNextPage();
                        }
                    }
                    isLoading = false;
                }
            });

            observer.observe(nextElement);
        } else if (loadMoreButton) {
            loadMoreButton.addEventListener('click', loadNextPage);
        }
    }
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant