Shopify Customer Slider Section

{% comment %}
  Section: Customer Reviews
  File: sections/customer-reviews.liquid
{% endcomment %}

<div class="reviews-section">
  {% if section.settings.section_title != blank %}
    <div class="reviews-header">
      {% if section.settings.eyebrow != blank %}
        <p class="reviews-eyebrow">{{ section.settings.eyebrow }}</p>
      {% endif %}
      <h2 class="reviews-title">{{ section.settings.section_title }}</h2>
      {% if section.settings.section_subtitle != blank %}
        <p class="reviews-subtitle">{{ section.settings.section_subtitle }}</p>
      {% endif %}
    </div>
  {% endif %}

  <div class="reviews-carousel-wrapper">
    <button class="reviews-nav reviews-nav--prev" aria-label="Previous">&#8592;</button>
    <div class="reviews-track-outer">
      <div class="reviews-track" id="reviews-track-{{ section.id }}">
        {% for block in section.blocks %}
          {% if block.type == 'review' %}
            <div class="review-card" {{ block.shopify_attributes }}>
              <div class="review-stars">
                {% assign star_count = block.settings.rating %}
                {% for i in (1..5) %}
                  {% if i <= star_count %}
                    <span class="star star--filled">★</span>
                  {% else %}
                    <span class="star star--empty">★</span>
                  {% endif %}
                {% endfor %}
              </div>
              {% if block.settings.review_text != blank %}
                <p class="review-text">"{{ block.settings.review_text }}"</p>
              {% endif %}
              <div class="review-author">
                <div class="review-avatar">
                  {% if block.settings.avatar != blank %}
                    <img
                      src="{{ block.settings.avatar | image_url: width: 80 }}"
                      alt="{{ block.settings.author_name | escape }}"
                      loading="lazy"
                    >
                  {% else %}
                    <span class="review-avatar-initials">
                      {{ block.settings.author_name | upcase | slice: 0, 2 }}
                    </span>
                  {% endif %}
                </div>
                <div class="review-author-info">
                  {% if block.settings.author_name != blank %}
                    <strong class="review-author-name">{{ block.settings.author_name }}</strong>
                  {% endif %}
                  {% if block.settings.author_location != blank %}
                    <span class="review-author-location">{{ block.settings.author_location }}</span>
                  {% endif %}
                </div>
              </div>
            </div>
          {% endif %}
        {% endfor %}
      </div>
    </div>
    <button class="reviews-nav reviews-nav--next" aria-label="Next">&#8594;</button>
  </div>

  <div class="reviews-dots" id="reviews-dots-{{ section.id }}">
    {% for block in section.blocks %}
      {% if block.type == 'review' %}
        <button
          class="reviews-dot {% if forloop.first %}reviews-dot--active{% endif %}"
          data-index="{{ forloop.index0 }}"
          aria-label="Go to review {{ forloop.index }}"
        ></button>
      {% endif %}
    {% endfor %}
  </div>
</div>

<style>
  .reviews-section {
    background-color: {{ section.settings.background_color }};
    padding: {{ section.settings.padding_top }}px 32px {{ section.settings.padding_bottom }}px;
  }

  .reviews-header {
    text-align: center;
    margin-bottom: 40px;
  }

  .reviews-eyebrow {
    font-size: 11px;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: {{ section.settings.eyebrow_color }};
    margin: 0 0 10px;
  }

  .reviews-title {
    font-size: {{ section.settings.title_size }}px;
    color: {{ section.settings.title_color }};
    line-height: 1.25;
    margin: 0 0 10px;
  }

  .reviews-subtitle {
    font-size: 14px;
    color: {{ section.settings.eyebrow_color }};
    margin: 0;
    opacity: 0.8;
  }

  .reviews-carousel-wrapper {
    max-width: {{ section.settings.max_width }}px;
    margin: 0 auto;
    display: flex;
    align-items: center;
    gap: 12px;
  }

  .reviews-track-outer {
    flex: 1;
    overflow: hidden;
  }

  .reviews-track {
    display: flex;
    gap: {{ section.settings.card_gap }}px;
    transition: transform 0.5s ease;
    will-change: transform;
  }

  .review-card {
    background-color: {{ section.settings.card_bg }};
    border: 1px solid {{ section.settings.card_border_color }};
    padding: {{ section.settings.card_padding }}px;
    display: flex;
    flex-direction: column;
    gap: 16px;
    flex-shrink: 0;
    box-sizing: border-box;
  }

  .review-stars {
    display: flex;
    gap: 2px;
  }

  .star {
    font-size: {{ section.settings.star_size }}px;
    line-height: 1;
  }

  .star--filled { color: {{ section.settings.star_color }}; }
  .star--empty  { color: {{ section.settings.card_border_color }}; }

  .review-text {
    font-size: {{ section.settings.review_text_size }}px;
    color: {{ section.settings.review_text_color }};
    line-height: 1.75;
    margin: 0;
    font-style: italic;
    flex: 1;
  }

  .review-author {
    display: flex;
    align-items: center;
    gap: 12px;
    padding-top: 8px;
    border-top: 1px solid {{ section.settings.card_border_color }};
  }

  .review-avatar {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    overflow: hidden;
    background-color: {{ section.settings.avatar_bg }};
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
  }

  .review-avatar img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .review-avatar-initials {
    font-size: 13px;
    font-weight: 500;
    color: {{ section.settings.avatar_text_color }};
    letter-spacing: .04em;
  }

  .review-author-info {
    display: flex;
    flex-direction: column;
    gap: 2px;
  }

  .review-author-name {
    font-size: 13px;
    font-weight: 600;
    color: {{ section.settings.author_name_color }};
    display: block;
  }

  .review-author-location {
    font-size: 12px;
    color: {{ section.settings.author_location_color }};
    opacity: 0.75;
  }

  .reviews-nav {
    background: {{ section.settings.card_bg }};
    border: 1px solid {{ section.settings.card_border_color }};
    color: {{ section.settings.title_color }};
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    font-size: 18px;
    flex-shrink: 0;
    transition: background .2s;
  }

  .reviews-nav:hover {
    background: {{ section.settings.card_border_color }};
  }

  .reviews-dots {
    display: flex;
    justify-content: center;
    gap: 8px;
    margin-top: 24px;
  }

  .reviews-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    border: none;
    background: {{ section.settings.card_border_color }};
    cursor: pointer;
    padding: 0;
    transition: all .3s;
  }

  .reviews-dot--active {
    background: {{ section.settings.star_color }};
    width: 24px;
    border-radius: 4px;
  }

  @media (max-width: 749px) {
    .reviews-section {
      padding-left: 16px;
      padding-right: 16px;
    }
    .reviews-nav { display: none; }
  }
</style>

<script>
(function () {
  var SECTION_ID   = '{{ section.id }}';
  var VISIBLE      = {{ section.settings.cards_visible }};
  var GAP          = {{ section.settings.card_gap }};
  var AUTO_DELAY   = {{ section.settings.autoplay_speed }} * 1000;

  var track    = document.getElementById('reviews-track-' + SECTION_ID);
  var dotsWrap = document.getElementById('reviews-dots-' + SECTION_ID);
  var section  = track.closest('.reviews-section');
  var cards    = Array.from(track.querySelectorAll('.review-card'));
  var dots     = Array.from(dotsWrap.querySelectorAll('.reviews-dot'));
  var total    = cards.length;
  var current  = 0;
  var timer    = null;

  function visibleCount() {
    return window.innerWidth <= 749 ? 1 : VISIBLE;
  }

  function setCardWidths() {
    var vis   = visibleCount();
    var outer = track.parentElement.offsetWidth;
    var w     = (outer - GAP * (vis - 1)) / vis;
    cards.forEach(function (c) {
      c.style.width = w + 'px';
    });
  }

  function maxIndex() {
    return Math.max(0, total - visibleCount());
  }

  function goTo(index) {
    current = ((index % total) + total) % total;
    if (current > maxIndex()) current = 0;

    var cardWidth = cards[0].offsetWidth;
    var offset    = current * (cardWidth + GAP);
    track.style.transform = 'translateX(-' + offset + 'px)';

    dots.forEach(function (d, i) {
      d.classList.toggle('reviews-dot--active', i === current);
    });
  }

  function startAuto() {
    stopAuto();
    timer = setInterval(function () {
      goTo(current + 1);
    }, AUTO_DELAY);
  }

  function stopAuto() {
    if (timer) clearInterval(timer);
  }

  section.querySelector('.reviews-nav--prev').addEventListener('click', function () {
    goTo(current - 1);
    stopAuto(); startAuto();
  });

  section.querySelector('.reviews-nav--next').addEventListener('click', function () {
    goTo(current + 1);
    stopAuto(); startAuto();
  });

  dots.forEach(function (dot) {
    dot.addEventListener('click', function () {
      goTo(parseInt(dot.getAttribute('data-index')));
      stopAuto(); startAuto();
    });
  });

  /* pause on hover */
  section.addEventListener('mouseenter', stopAuto);
  section.addEventListener('mouseleave', startAuto);

  /* touch swipe */
  var touchStartX = 0;
  track.addEventListener('touchstart', function (e) {
    touchStartX = e.touches[0].clientX;
    stopAuto();
  }, { passive: true });
  track.addEventListener('touchend', function (e) {
    var diff = touchStartX - e.changedTouches[0].clientX;
    if (Math.abs(diff) > 40) goTo(diff > 0 ? current + 1 : current - 1);
    startAuto();
  }, { passive: true });

  window.addEventListener('resize', function () {
    setCardWidths();
    goTo(current);
  });

  setCardWidths();
  goTo(0);
  startAuto();
})();
</script>

{% schema %}
{
  "name": "Customer Reviews",
  "tag": "section",
  "class": "section-customer-reviews",
  "max_blocks": 9,
  "settings": [
    {
      "type": "header",
      "content": "Background & Layout"
    },
    {
      "type": "color",
      "id": "background_color",
      "label": "Background color",
      "default": "#FAF8F3"
    },
    {
      "type": "range",
      "id": "max_width",
      "min": 600,
      "max": 1400,
      "step": 20,
      "unit": "px",
      "label": "Section max width",
      "default": 1100
    },
    {
      "type": "range",
      "id": "padding_top",
      "min": 0,
      "max": 120,
      "step": 4,
      "unit": "px",
      "label": "Padding top",
      "default": 64
    },
    {
      "type": "range",
      "id": "padding_bottom",
      "min": 0,
      "max": 120,
      "step": 4,
      "unit": "px",
      "label": "Padding bottom",
      "default": 64
    },
    {
      "type": "header",
      "content": "Section Header"
    },
    {
      "type": "text",
      "id": "eyebrow",
      "label": "Eyebrow text",
      "default": "Happy Customers"
    },
    {
      "type": "text",
      "id": "section_title",
      "label": "Section title",
      "default": "What people are saying"
    },
    {
      "type": "text",
      "id": "section_subtitle",
      "label": "Subtitle (optional)"
    },
    {
      "type": "header",
      "content": "Header Colors"
    },
    {
      "type": "color",
      "id": "eyebrow_color",
      "label": "Eyebrow color",
      "default": "#8A7A5C"
    },
    {
      "type": "color",
      "id": "title_color",
      "label": "Title color",
      "default": "#3B3226"
    },
    {
      "type": "range",
      "id": "title_size",
      "min": 20,
      "max": 48,
      "step": 2,
      "unit": "px",
      "label": "Title font size",
      "default": 32
    },
    {
      "type": "header",
      "content": "Carousel"
    },
    {
      "type": "range",
      "id": "cards_visible",
      "min": 1,
      "max": 4,
      "step": 1,
      "label": "Cards visible at once",
      "default": 3
    },
    {
      "type": "range",
      "id": "autoplay_speed",
      "min": 2,
      "max": 10,
      "step": 1,
      "unit": "s",
      "label": "Auto-slide speed",
      "default": 4
    },
    {
      "type": "header",
      "content": "Cards"
    },
    {
      "type": "color",
      "id": "card_bg",
      "label": "Card background",
      "default": "#FFFFFF"
    },
    {
      "type": "color",
      "id": "card_border_color",
      "label": "Card border color",
      "default": "#E3DDD0"
    },
    {
      "type": "range",
      "id": "card_gap",
      "min": 8,
      "max": 40,
      "step": 4,
      "unit": "px",
      "label": "Gap between cards",
      "default": 20
    },
    {
      "type": "range",
      "id": "card_padding",
      "min": 12,
      "max": 40,
      "step": 4,
      "unit": "px",
      "label": "Card padding",
      "default": 24
    },
    {
      "type": "header",
      "content": "Stars"
    },
    {
      "type": "color",
      "id": "star_color",
      "label": "Star color",
      "default": "#8A7A5C"
    },
    {
      "type": "range",
      "id": "star_size",
      "min": 12,
      "max": 24,
      "step": 1,
      "unit": "px",
      "label": "Star size",
      "default": 16
    },
    {
      "type": "header",
      "content": "Review Text"
    },
    {
      "type": "color",
      "id": "review_text_color",
      "label": "Review text color",
      "default": "#6B5C40"
    },
    {
      "type": "range",
      "id": "review_text_size",
      "min": 12,
      "max": 18,
      "step": 1,
      "unit": "px",
      "label": "Review text size",
      "default": 14
    },
    {
      "type": "header",
      "content": "Author"
    },
    {
      "type": "color",
      "id": "avatar_bg",
      "label": "Avatar background",
      "default": "#F0EBE0"
    },
    {
      "type": "color",
      "id": "avatar_text_color",
      "label": "Avatar initials color",
      "default": "#8A7A5C"
    },
    {
      "type": "color",
      "id": "author_name_color",
      "label": "Author name color",
      "default": "#3B3226"
    },
    {
      "type": "color",
      "id": "author_location_color",
      "label": "Author location color",
      "default": "#8A7A5C"
    }
  ],
  "blocks": [
    {
      "type": "review",
      "name": "Review",
      "settings": [
        {
          "type": "range",
          "id": "rating",
          "min": 1,
          "max": 5,
          "step": 1,
          "label": "Star rating",
          "default": 5
        },
        {
          "type": "textarea",
          "id": "review_text",
          "label": "Review text",
          "default": "দেখতে যত সুন্দর, ব্যবহারেও তত কাজের।"
        },
        {
          "type": "text",
          "id": "author_name",
          "label": "Author name",
          "default": "Rafiq Ahmed"
        },
        {
          "type": "text",
          "id": "author_location",
          "label": "Author location",
          "default": "Dhaka, Dhanmondi"
        },
        {
          "type": "image_picker",
          "id": "avatar",
          "label": "Author photo (optional)",
          "info": "If left empty initials will show instead"
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "Customer Reviews",
      "blocks": [
        {
          "type": "review",
          "settings": {
            "rating": 5,
            "review_text": "দেখতে যত সুন্দর, ব্যবহারেও তত কাজের। Key holder টা দরজার পাশে রাখার পর থেকে চাবি আর হারাই না।",
            "author_name": "Rafiq Ahmed",
            "author_location": "Dhaka, Dhanmondi"
          }
        },
        {
          "type": "review",
          "settings": {
            "rating": 5,
            "review_text": "The Half-Moon shelf with the glass vase is exactly what my living room needed. Looks like something from an interior design magazine.",
            "author_name": "Sadia Rahman",
            "author_location": "Dhaka, Gulshan"
          }
        },
        {
          "type": "review",
          "settings": {
            "rating": 5,
            "review_text": "Packaging was beautiful — came as a gift for my sister's new apartment. She loved it. The Globe Vase L-Stand is perfect for her desk.",
            "author_name": "Nadia Khan",
            "author_location": "Chattogram"
          }
        }
      ]
    }
  ]
}
{% endschema %}

Leave a Reply

Your email address will not be published. Required fields are marked *