Sync List Carousel with Google Sheets

Description

  • Sync List Carousel section with Google Sheets

#1. First, you need to add a List People Section

#2. Choose Carousel

#3. Create a Google Sheets file with Columns like this.

#4. Next, copy this string from Google Sheets URL

and Tab Name

#5. Use this code to Page Header Injection

<script>
class SheetsCarouselSync {
    constructor(options) {
        this.sheetsId = options.sheetsId || '1RN4zvG4X1JdFQQgz0gS2WwfYc5XF-gGpYqdB4T7WX70';
        this.sheetName = options.sheetName || 'test data';
        this.config = options;
        this.carouselSection = this.config.target;
        this.carouselSection.dataset.sheetsSync = "loading";
        this.sheetsData = [];
        this.carouselItems = this.carouselSection.querySelectorAll("li.user-items-list-carousel__slide");
        this.carouselContainer = this.carouselSection.querySelector(".user-items-list-carousel__slides");
        this.initialize();
    }

    async initialize() {
        try {
            this.sheetsData = await this.fetchSheetsData();
            console.log("Fetched sheets data:", this.sheetsData.length);
            console.log("Current carousel items:", this.carouselItems.length);

            while (this.carouselItems.length < this.sheetsData.length) {
                console.log("Creating new slide...");
                const newSlide = this.createNewSlide();
                this.carouselContainer.appendChild(newSlide);
                this.carouselItems = this.carouselSection.querySelectorAll("li.user-items-list-carousel__slide");
                console.log("Total slides now:", this.carouselItems.length);
            }

            if (this.carouselItems.length > this.sheetsData.length) {
                console.warn("Too many carousel items, removing excess");
                while (this.carouselItems.length > this.sheetsData.length) {
                    this.carouselItems[this.carouselItems.length - 1].remove();
                    this.carouselItems = this.carouselSection.querySelectorAll("li.user-items-list-carousel__slide");
                }
            }

            this.createItemTemplate();
            this.populateCarouselItems();
            this.carouselSection.dataset.sheetsSync = "complete";
            console.log("Sync completed with", this.carouselItems.length, "items");
        } catch (error) {
            console.error("Sheets Carousel Sync failed:", error);
            this.carouselSection.dataset.sheetsSync = "error";
        }
    }

    async fetchSheetsData() {
        try {
            const csvUrl = `https://docs.google.com/spreadsheets/d/${this.sheetsId}/gviz/tq?tqx=out:csv&sheet=${encodeURIComponent(this.sheetName)}`;
            const response = await fetch(csvUrl);
            
            if (!response.ok) {
                throw new Error(`CSV request failed: ${response.status}`);
            }

            const csvText = await response.text();
            return this.parseCSVData(csvText);
        } catch (error) {
            console.error("Error loading sheets data:", error);
            throw error;
        }
    }

    parseCSVData(csvText) {
        const rows = csvText.split('\n').filter(row => row.trim());
        const data = [];
        
        if (rows.length < 2) return data;

        const headers = this.parseCSVRow(rows[0]).map(header => header.replace(/"/g, '').trim());
        console.log("Headers found:", headers);
        console.log("Total rows:", rows.length);
        for (let i = 1; i < rows.length; i++) {
            const row = rows[i];
            if (row.trim()) {
                const cells = this.parseCSVRow(row);
                const rowData = {};
                
                headers.forEach((header, index) => {
                    rowData[header] = cells[index] ? cells[index].replace(/"/g, '').trim() : '';
                });

                console.log(`Row ${i}:`, rowData);
                if (rowData.Title && rowData.Title.trim()) {
                    data.push({
                        title: rowData.Title || '',
                        description: rowData.Description || '',
                        imageUrl: rowData['Image URL'] || '',
                        buttonUrl: rowData['Button URL'] || '#'
                    });
                    console.log(`Added item ${data.length}:`, data[data.length - 1]);
                } else {
                    console.log(`Skipped row ${i} - no title:`, rowData);
                }
            }
        }

        console.log("Final data array length:", data.length);
        return data;
    }

    parseCSVRow(row) {
        const cells = [];
        let current = '';
        let inQuotes = false;
        
        for (let i = 0; i < row.length; i++) {
            const char = row[i];
            
            if (char === '"') {
                inQuotes = !inQuotes;
            } else if (char === ',' && !inQuotes) {
                cells.push(current);
                current = '';
            } else {
                current += char;
            }
        }
        
        cells.push(current);
        return cells;
    }

    createNewSlide() {
        const slideTemplate = `
            <li class="user-items-list-carousel__slide list-item" data-is-card-enabled="false">
                <div class="user-items-list-carousel__media-container" style="margin-bottom: 4%; width: 100%;">
                    <div class="user-items-list-carousel__media-inner preFade fadeIn" data-media-aspect-ratio="3:2" data-animation-role="image">
                        <img class="user-items-list-carousel__media" data-load="false" data-mode="cover" data-use-advanced-positioning="true" style="width: 100%; height: 100%; object-fit: cover;" data-parent-ratio="1.5" loading="lazy" decoding="async" data-loader="sqs">
                    </div>
                </div>
                <div class="list-item-content">
                    <div class="list-item-content__text-wrapper">
                        <h2 class="list-item-content__title preFade" style="max-width: 100%;"></h2>
                        <div class="list-item-content__description" style="margin-top: 10px; max-width: 100%;"></div>
                    </div>
                    <div class="list-item-content__button-wrapper">
                        <div class="list-item-content__button-container" style="margin-top: 10px; max-width: 100%;" data-animation-role="button">
                            <a class="list-item-content__button sqs-block-button-element sqs-block-button-element--medium sqs-button-element--primary" href="#">
                                Xem chi tiết
                            </a>
                        </div>
                    </div>
                </div>
            </li>
        `;
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = slideTemplate;
        return tempDiv.firstElementChild;
    }

    createItemTemplate() {
        if (this.carouselItems.length > 0) {
            const templateHTML = this.carouselItems[0].innerHTML;
            this.carouselItems.forEach(item => {
                item.innerHTML = templateHTML;
            });
        }
    }

    populateCarouselItems() {
        console.log("Populating", this.carouselItems.length, "items with", this.sheetsData.length, "sheets data");
        this.carouselItems = this.carouselSection.querySelectorAll("li.user-items-list-carousel__slide");
        console.log("Refreshed carousel items count:", this.carouselItems.length);
        
        for (let index = 0; index < this.sheetsData.length; index++) {
            if (!this.carouselItems[index]) {
                console.log("No carousel item for index", index);
                continue;
            }

            console.log("Processing data", index, ":", this.sheetsData[index].title);
            
            const carouselItem = this.carouselItems[index];
            const {
                title: itemTitle,
                description: itemDescription,
                imageUrl: itemImage,
                buttonUrl: itemLink
            } = this.sheetsData[index];

            let titleElement = carouselItem.querySelector(".list-item-content__title");
            let descriptionElement = carouselItem.querySelector(".list-item-content__description");
            let imageElement = carouselItem.querySelector("img");
            let buttonElement = carouselItem.querySelector(".list-item-content__button");

            if (titleElement) {
                if (this.config.linkTitle && itemLink && itemLink !== '#') {
                    titleElement.innerHTML = `<a href="${itemLink}">${itemTitle}</a>`;
                } else {
                    titleElement.innerHTML = `<span>${itemTitle}</span>`;
                }
                console.log("Updated title for item", index);
            }

            if (descriptionElement && itemDescription) {
                descriptionElement.innerHTML = `<p style="white-space: pre-wrap;">${itemDescription}</p>`;
                console.log("Updated description for item", index);
            }

            if (imageElement && itemImage) {
                let newImage = imageElement.cloneNode(true);
                newImage.src = itemImage;
                newImage.dataset.src = itemImage;
                newImage.dataset.image = itemImage;
                newImage.srcset = "";
                newImage.alt = itemTitle;
                imageElement.parentElement.append(newImage);
                imageElement.style.display = "none";
                console.log("Updated image for item", index);
            }

            if (buttonElement) {
                buttonElement.textContent = "Download";
                if (itemLink && itemLink !== '#') {
                    buttonElement.href = itemLink;
                } else {
                    buttonElement.href = "#";
                }
                console.log("Updated button for item", index);
            }
        }

        window.dispatchEvent(new Event("resize"));
    }

    setupEventHandlers() {
        window.addEventListener("DOMContentLoaded", () => {
            this.populateCarouselItems();
        });

        window.addEventListener("load", () => {
            this.populateCarouselItems();
        });
    }

    handleCarouselController() {
        const controllerElement = this.carouselSection.querySelector("[data-controller]");
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.attributeName === "data-controllers-bound" && 
                    controllerElement.dataset.controllersBound === "UserItemsListCarousel") {
                    controllerElement.removeAttribute("data-controller");
                    observer.disconnect();
                }
            });
        });

        if (controllerElement && controllerElement.dataset.controllersBound === "UserItemsListCarousel") {
            controllerElement.removeAttribute("data-controller");
        } else if (controllerElement) {
            observer.observe(controllerElement, {
                attributes: true,
                attributeFilter: ["data-controllers-bound"]
            });
        }
    }
}

(function() {
    const initSheetsCarouselSync = () => {
        const carouselSections = document.querySelectorAll(".user-items-list-carousel");
        
        carouselSections.forEach(section => {
            if (section.dataset.sheetsSync) return;
            
            const config = {
                target: section.closest(".page-section"),
                linkTitle: true,
                linkImage: false
            };

            if (config.target) {
                config.target.SheetsCarouselSync = new SheetsCarouselSync(config);
            } else {
                section.dataset.sheetsSync = "no-target";
            }
        });
    };

    window.sheetsCarouselSync = {
        init: initSheetsCarouselSync,
        refreshAll: () => {
            const carouselSections = document.querySelectorAll(".user-items-list-carousel");
            carouselSections.forEach(section => {
                const target = section.closest(".page-section");
                if (target && target.SheetsCarouselSync) {
                    target.SheetsCarouselSync.refreshData();
                }
            });
        }
    };

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initSheetsCarouselSync);
    } else {
        initSheetsCarouselSync();
    }

    setInterval(() => {
        window.sheetsCarouselSync.refreshAll();
    }, 30000);
})();
</script>

#6. Update string and tab name you have in step #4

 

#7. If you want to do this, but on another platform, or coding new Carousel, you can edit page > Add a Code Block

Then use these syntax into Code Block (or HTML field with other platforms)

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
<div class="carousel-container" id="carouselContainer">
  <div class="carousel-wrapper">
    <button class="arrow-button arrow-left" onclick="moveSlide(-1)" aria-label="Previous">
      <div class="arrow-background"></div>
      <i class="fas fa-chevron-left"></i>
    </button>
    
    <div class="carousel-track" id="carouselTrack">
      <div class="carousel-item">
        <div class="report-card">
          <img src="https://images.squarespace-cdn.com/content/v1/6846edf34bfaf3462330bfc2/ad178611-96a5-4a7b-898b-48962b5cd139/2025Report.png?format=750w" alt="Loading...">
          <h3>Loading...</h3>
          <p>Please wait while we load data from Google Sheets...</p>
          <button class="download-btn" onclick="window.open('#', '_blank')">DOWNLOAD</button>
        </div>
      </div>
    </div>
    
    <button class="arrow-button arrow-right" onclick="moveSlide(1)" aria-label="Next">
      <div class="arrow-background"></div>
      <i class="fa-solid fa-arrow-right"></i>
    </button>
  </div>
  
  <div class="loading-indicator" id="loadingIndicator">
    <div class="loading-spinner"></div>
    <p>Loading data from Google Sheets...</p>
  </div>
</div>

<style>
.carousel-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  position: relative;
}

.carousel-wrapper {
  position: relative;
  overflow: hidden;
  border-radius: 12px;
}

.carousel-track {
  display: flex;
  transition: transform 0.3s ease;
  cursor: grab;
  user-select: none;
}

.carousel-track:active {
  cursor: grabbing;
}

.carousel-item {
  flex: 0 0 25%;
  padding: 0 10px;
  box-sizing: border-box;
}

.report-card {
  background: #e6e4e9;
  border-radius: 12px;
  transition: transform 0.3s ease, box-shadow 0.3s ease;
  height: 100%;
  display: flex;
  flex-direction: column;
  text-align: center;
}

.report-card:hover {
  transform: translateY(-5px);
}

.report-card img {
  width: 100%;
  height: 250px;
  object-fit: cover;
  display: block;
}

.report-card h3 {
  font-size: 20px;
  font-weight: bold;
  margin: 20px 20px 10px;
  color: #333;
  line-height: 1.3;
}

.report-card p {
  font-size: 14px;
  color: #666;
  margin: 0 20px 20px;
  line-height: 1.4;
  flex-grow: 1;
}

.download-btn {
  margin: 0 20px 20px;
  padding: 12px 24px;
  background: transparent;
  border: 2px solid #333;
  border-radius: 25px;
  font-weight: bold;
  font-size: 12px;
  letter-spacing: 1px;
  cursor: pointer;
  transition: all 0.3s ease;
  color: #333;
}

.download-btn:hover {
  background: #333;
  color: white;
  transform: translateY(-2px);
}

.arrow-button {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 60px;
  height: 60px;
  background: rgba(255,255,255,0.9);
  border: none;
  border-radius: 50%;
  cursor: pointer;
  z-index: 10;
  transition: all 0.3s ease;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}

.arrow-button:hover {
  background: white;
  box-shadow: 0 6px 20px rgba(0,0,0,0.15);
  transform: translateY(-50%) scale(1.1);
}

.arrow-button i {
  font-size: 18px;
  color: #333;
}

.arrow-left {
  left: 0px;
}

.arrow-right {
  right: 0px;
}

.arrow-background {
  position: absolute;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: rgba(0,0,0,0.05);
  opacity: 0;
  transition: opacity 0.3s ease;
}

.arrow-button:hover .arrow-background {
  opacity: 1;
}

.loading-indicator {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
  background: rgba(255,255,255,0.95);
  padding: 30px;
  border-radius: 12px;
  box-shadow: 0 4px 20px rgba(0,0,0,0.1);
  z-index: 20;
}

.loading-spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #8BC34A;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin: 0 auto 15px;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.loading-indicator p {
  margin: 0;
  color: #666;
  font-size: 14px;
}

.carousel-container[data-sheets-sync="complete"] .loading-indicator {
  display: none;
}

.carousel-container[data-sheets-sync="error"] .loading-indicator p {
  color: #d32f2f;
}

.carousel-container[data-sheets-sync="error"] .loading-indicator p:after {
  content: " Error occurred while loading data!";
}

@media (max-width: 768px) {
  .carousel-item {
    flex: 0 0 50%;
  }
  
  .arrow-button {
    width: 50px;
    height: 50px;
  }
  
  .arrow-button i {
    font-size: 16px;
  }
  
  .arrow-left {
    left: -25px;
  }
  
  .arrow-right {
    right: -25px;
  }
}

@media (max-width: 480px) {
  .carousel-container {
    padding: 10px;
  }
  
  .carousel-item {
    padding: 0 5px;
  }
  
  .report-card h3 {
    font-size: 18px;
    margin: 15px 15px 8px;
  }
  
  .report-card p {
    font-size: 13px;
    margin: 0 15px 15px;
  }
  
  .download-btn {
    margin: 0 15px 15px;
    padding: 10px 20px;
    font-size: 11px;
  }
}
</style>

<script>
class SheetsCarouselSync {
    constructor(options) {
        this.sheetsId = options.sheetsId || '1RN4zvG4X1JdFQQgz0gS2WwfYc5XF-gGpYqdB4T7WX70';
        this.sheetName = options.sheetName || 'test data';
        this.config = options;
        this.carouselContainer = this.config.target;
        this.carouselContainer.dataset.sheetsSync = "loading";
        this.sheetsData = [];
        this.carouselTrack = this.carouselContainer.querySelector("#carouselTrack");
        this.initialize();
    }

    async initialize() {
        try {
            console.log("Starting to load data from Google Sheets...");
            this.sheetsData = await this.fetchSheetsData();
            console.log("Loaded", this.sheetsData.length, "items from Google Sheets");

            if (this.sheetsData.length > 0) {
                this.createCarouselItems();
                this.carouselContainer.dataset.sheetsSync = "complete";
                console.log("Sync completed!");
                
                setTimeout(() => {
                    window.updateCarousel && window.updateCarousel();
                }, 100);
            } else {
                throw new Error("No data from Google Sheets");
            }
        } catch (error) {
            console.error("Error syncing Google Sheets:", error);
            this.carouselContainer.dataset.sheetsSync = "error";
        }
    }

    async fetchSheetsData() {
        try {
            const csvUrl = `https://docs.google.com/spreadsheets/d/${this.sheetsId}/gviz/tq?tqx=out:csv&sheet=${encodeURIComponent(this.sheetName)}`;
            console.log("Loading from:", csvUrl);
            
            const response = await fetch(csvUrl);
            
            if (!response.ok) {
                throw new Error(`CSV request failed: ${response.status}`);
            }

            const csvText = await response.text();
            return this.parseCSVData(csvText);
        } catch (error) {
            console.error("Error loading sheets data:", error);
            throw error;
        }
    }

    parseCSVData(csvText) {
        const rows = csvText.split('\n').filter(row => row.trim());
        const data = [];
        
        if (rows.length < 2) return data;

        const headers = this.parseCSVRow(rows[0]).map(header => header.replace(/"/g, '').trim());
        console.log("Headers found:", headers);

        for (let i = 1; i < rows.length; i++) {
            const row = rows[i];
            if (row.trim()) {
                const cells = this.parseCSVRow(row);
                const rowData = {};
                
                headers.forEach((header, index) => {
                    rowData[header] = cells[index] ? cells[index].replace(/"/g, '').trim() : '';
                });

                if (rowData.Title && rowData.Title.trim()) {
                    data.push({
                        title: rowData.Title || '',
                        description: rowData.Description || '',
                        imageUrl: rowData['Image URL'] || 'https://via.placeholder.com/300x400?text=No+Image',
                        buttonUrl: rowData['Button URL'] || '#'
                    });
                }
            }
        }

        return data;
    }

    parseCSVRow(row) {
        const cells = [];
        let current = '';
        let inQuotes = false;
        
        for (let i = 0; i < row.length; i++) {
            const char = row[i];
            
            if (char === '"') {
                inQuotes = !inQuotes;
            } else if (char === ',' && !inQuotes) {
                cells.push(current);
                current = '';
            } else {
                current += char;
            }
        }
        
        cells.push(current);
        return cells;
    }

    createCarouselItems() {
        this.carouselTrack.innerHTML = '';
        
        this.sheetsData.forEach((item, index) => {
            const carouselItem = document.createElement('div');
            carouselItem.className = 'carousel-item';
            
            carouselItem.innerHTML = `
                <div class="report-card">
                    <img src="${item.imageUrl}" alt="${item.title}" onerror="this.src='https://via.placeholder.com/300x400?text=Image+Error'">
                    <h3>${item.title}</h3>
                    <p>${item.description}</p>
                    <button class="download-btn" onclick="window.open('${item.buttonUrl}', '_blank')">DOWNLOAD</button>
                </div>
            `;
            
            this.carouselTrack.appendChild(carouselItem);
            console.log(`Created item ${index + 1}: ${item.title}`);
        });
    }
}

let currentSlide = 0;
let isDragging = false;
let startX = 0;
let currentX = 0;
let threshold = 50;

const track = document.getElementById('carouselTrack');

function getItemsPerView() {
    return window.innerWidth <= 768 ? 2 : 4;
}

function updateCarousel() {
    const items = document.querySelectorAll('.carousel-item');
    const totalItems = items.length;
    
    if (totalItems === 0) return;
    
    const itemsPerView = getItemsPerView();
    const maxSlide = Math.max(0, totalItems - itemsPerView);
    
    if (currentSlide > maxSlide) {
        currentSlide = maxSlide;
    }
    
    const translateX = -currentSlide * (100 / itemsPerView);
    track.style.transform = `translateX(${translateX}%)`;
}

function moveSlide(direction) {
    const items = document.querySelectorAll('.carousel-item');
    const itemsPerView = getItemsPerView();
    const maxSlide = Math.max(0, items.length - itemsPerView);
    
    currentSlide += direction;
    
    if (currentSlide < 0) {
        currentSlide = 0;
    } else if (currentSlide > maxSlide) {
        currentSlide = maxSlide;
    }
    
    updateCarousel();
}

track.addEventListener('mousedown', (e) => {
    isDragging = true;
    startX = e.clientX;
    track.style.cursor = 'grabbing';
    track.style.transition = 'none';
});

track.addEventListener('mousemove', (e) => {
    if (!isDragging) return;
    e.preventDefault();
    currentX = e.clientX - startX;
});

track.addEventListener('mouseup', () => {
    if (!isDragging) return;
    isDragging = false;
    track.style.cursor = 'grab';
    track.style.transition = 'transform 0.3s ease';
    
    if (Math.abs(currentX) > threshold) {
        if (currentX > 0) {
            moveSlide(-1);
        } else {
            moveSlide(1);
        }
    }
    
    currentX = 0;
});

track.addEventListener('mouseleave', () => {
    if (isDragging) {
        isDragging = false;
        track.style.cursor = 'grab';
        track.style.transition = 'transform 0.3s ease';
        currentX = 0;
    }
});

track.addEventListener('touchstart', (e) => {
    isDragging = true;
    startX = e.touches[0].clientX;
    track.style.transition = 'none';
});

track.addEventListener('touchmove', (e) => {
    if (!isDragging) return;
    currentX = e.touches[0].clientX - startX;
});

track.addEventListener('touchend', () => {
    if (!isDragging) return;
    isDragging = false;
    track.style.transition = 'transform 0.3s ease';
    
    if (Math.abs(currentX) > threshold) {
        if (currentX > 0) {
            moveSlide(-1);
        } else {
            moveSlide(1);
        }
    }
    
    currentX = 0;
});

window.addEventListener('resize', updateCarousel);
window.updateCarousel = updateCarousel;

(function() {
    const initSheetsCarouselSync = () => {
        const carouselContainer = document.getElementById('carouselContainer');
        
        if (carouselContainer && !carouselContainer.dataset.sheetsSync) {
            const config = {
                target: carouselContainer,
                linkTitle: true,
                linkImage: false
            };

            new SheetsCarouselSync(config);
        }
    };

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initSheetsCarouselSync);
    } else {
        initSheetsCarouselSync();
    }
})();
</script>

Remember to update Sheets string & tab name

Result like this

 

Buy me a coffee