Summary single project carousel

To achieve Summary Block single carousel for Portfolio Page, like this.

#1. First, find Portfolio Page URL

#2. Next, add a Summary Block Carousel

#3. Enter Portfolio Page URL in Header text

#4. Use this code to Page Header Injection

<script>
const PortfolioCardConverter = {
  initialize() {
    document.addEventListener('DOMContentLoaded', () => {
      setTimeout(() => {
        this.convertSummaryBlocks();
      }, 1000);
    });
  },

  convertSummaryBlocks() {
    const summaryBlocks = document.querySelectorAll('.summary-block-wrapper');
    summaryBlocks.forEach((block) => {
      const headerText = block.querySelector('.summary-header-text');
      if (headerText && headerText.textContent.includes('/projects')) {
        this.convertToCardCarousel(block, headerText.textContent.trim());
      }
    });
  },

  async convertToCardCarousel(summaryBlock, headerText) {
    const headerElement = summaryBlock.querySelector('.summary-heading');
    const pagerElement = summaryBlock.querySelector('.summary-carousel-pager');
    const listContainer = summaryBlock.querySelector('.summary-item-list-container');

    if (headerElement) headerElement.style.display = 'none';
    if (pagerElement) pagerElement.style.display = 'none';

    if (!listContainer) return;

    listContainer.innerHTML = '<div style="text-align:center;padding:40px;color:#666;">Loading portfolio...</div>';

    try {
      const portfolioData = await this.fetchPortfolioData(headerText);
      if (portfolioData.length > 0) {
        listContainer.innerHTML = this.generateCardCarousel(portfolioData);
        this.initializeCardCarousel(listContainer);
      } else {
        listContainer.innerHTML = '<div style="text-align:center;padding:40px;color:#666;">No portfolio items found</div>';
      }
    } catch (error) {
      console.log('Error loading portfolio:', error);
      listContainer.innerHTML = '<div style="text-align:center;padding:40px;color:#666;">Error loading portfolio</div>';
    }
  },

  async fetchPortfolioData(headerText) {
    const projectsUrl = headerText.replace(/^\//, '');
    
    try {
      const response = await fetch('/' + projectsUrl);
      const html = await response.text();
      const parser = new DOMParser();
      const doc = parser.parseFromString(html, 'text/html');
      
      return this.extractPortfolioData(doc);
    } catch (error) {
      console.log('Fetch error:', error);
      return [];
    }
  },

  extractPortfolioData(doc) {
    const portfolioData = [];
    const gridItems = doc.querySelectorAll('.grid-item');
    
    const limitedItems = Array.from(gridItems).slice(0, 7);
    
    limitedItems.forEach((item) => {
      const titleElement = item.querySelector('.portfolio-title');
      const imgElement = item.querySelector('.grid-image img');
      const href = item.getAttribute('href');
      
      if (titleElement && imgElement && href) {
        const title = titleElement.textContent.trim();
        const image = imgElement.getAttribute('data-src') || imgElement.getAttribute('src') || imgElement.getAttribute('data-image');
        
        portfolioData.push({
          title: title,
          url: href,
          image: image
        });
      }
    });
    
    return portfolioData;
  },

  generateCardCarousel(portfolioData) {
    const items = portfolioData.map(item => `
      <div class="tp-item">
        <div class="tp-img" style="background-image:url('${item.image}')"></div>
        <a class="tp-desc" href="${item.url}">${item.title}</a>
        <p class="tp-foot" style="display:none;">${item.title}</p>
      </div>
    `).join('');

    return `
      <section class="tp-carousel">
        <div class="tp-card">
          <div class="tp-viewport">
            <div class="tp-track">
              ${items}
            </div>
          </div>
          <div class="tp-footer">
            <p class="tp-foot-text"></p>
            <div class="tp-arrows">
              <button class="tp-arrow tp-prev">
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
                  <path d="M15 18L9 12L15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                </svg>
              </button>
              <button class="tp-arrow tp-next">
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
                  <path d="M9 18L15 12L9 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                </svg>
              </button>
            </div>
          </div>
        </div>
      </section>
    `;
  },

  initializeCardCarousel(container) {
    setTimeout(() => {
      const track = container.querySelector('.tp-track');
      const slides = container.querySelectorAll('.tp-item').length;
      const foot = container.querySelector('.tp-foot-text');
      const prevBtn = container.querySelector('.tp-prev');
      const nextBtn = container.querySelector('.tp-next');
      
      if (!track || !foot || !prevBtn || !nextBtn || slides === 0) return;

      let i = 0;

      function update() {
        const current = container.querySelectorAll('.tp-item')[i];
        if (current) {
          foot.textContent = '';
          track.style.transform = 'translateX(' + (-i * 100) + '%)';
        }
      }

      function go(n) {
        i = (n + slides) % slides;
        update();
      }

      nextBtn.addEventListener('click', () => go(i + 1));
      prevBtn.addEventListener('click', () => go(i - 1));
      
      update();
    }, 100);
  }
};

PortfolioCardConverter.initialize();
</script>
<style>.summary-header-text,.summary-item-list{opacity:0}.tp-carousel{display:flex;justify-content:center;align-items:center;margin:40px auto}.tp-card{width:280px;height:500px;max-width:92vw;background:#fff;padding:28px;position:relative;display:flex;flex-direction:column;justify-content:space-between}.tp-viewport{overflow:hidden;flex:1}.tp-track{display:flex;transition:transform .4s ease;height:100%}.tp-item{flex:0 0 100%;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;row-gap:18px;height:100%}.tp-item .tp-foot{display:none}.tp-img{width:100%;height:280px;background-size:cover;background-position:center;background-repeat:no-repeat}.tp-desc{margin:0;text-align:center;font-size:16px;line-height:1.4;color:#111;font-weight:600;text-decoration:none;transition:opacity 0.3s ease;flex:1;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.tp-desc:hover{opacity:.7}.tp-footer{display:flex;align-items:center;justify-content:center;margin-top:18px}.tp-arrows{display:flex;gap:8px}.tp-arrow{width:36px;height:36px;border-radius:50%;border:1px solid rgb(0 0 0 / .15);background:#f8f8f8;cursor:pointer;display:grid;place-items:center;font-size:16px;line-height:1;transition:all 0.2s ease}.tp-arrow:hover{background:#e8e8e8;border-color:rgb(0 0 0 / .25)}.tp-arrow:active{transform:scale(.95)}@media (min-width:768px){.tp-card{width:320px;height:520px;padding:32px}.tp-img{height:340px}.tp-desc{font-size:18px}.tp-foot-text{font-size:18px}.tp-arrows{gap:10px}.tp-arrow{width:42px;height:42px;font-size:18px}}@media (max-width:767px){.tp-card{height:440px;padding:24px}.tp-img{height:240px}.tp-desc{font-size:14px}.tp-foot-text{font-size:14px}}
  .summary-header-text, .summary-item-list {opacity: 0;}
</style>

Buy me a coffee