<div x-data="initCarouselcarousel1019211284()" x-init="init()" class="relative w-full h-full mx-auto">

    <div class="flex justify-between items-center mb-8 ">
        <h2 class="text-3xl font-semibold ">Magasins à proximité</h2>

        <div class="flex items-center justify-center space-x-4 ">

            <div class=" hidden  sm:flex  flex space-x-2">
                <button type="button" class="carousel-button-prev-carousel-1019211284 before:border-none  btn btn-dark-subtle  btn-only-icon">
                    <svg class=" shrink-0" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path fill-rule="evenodd" clip-rule="evenodd" d="M9.46967 12.5303C9.17678 12.2374 9.17678 11.7626 9.46967 11.4697L13.4697 7.46967C13.7626 7.17678 14.2374 7.17678 14.5303 7.46967C14.8232 7.76256 14.8232 8.23744 14.5303 8.53033L11.0607 12L14.5303 15.4697C14.8232 15.7626 14.8232 16.2374 14.5303 16.5303C14.2374 16.8232 13.7626 16.8232 13.4697 16.5303L9.46967 12.5303Z" fill="currentColor" />
                    </svg>

                </button>
                <button type="button" class="carousel-button-next-carousel-1019211284 before:border-none  btn btn-dark-subtle  btn-only-icon">
                    <svg class=" shrink-0" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path fill-rule="evenodd" clip-rule="evenodd" d="M14.5303 11.4697C14.8232 11.7626 14.8232 12.2374 14.5303 12.5303L10.5303 16.5303C10.2374 16.8232 9.76256 16.8232 9.46967 16.5303C9.17678 16.2374 9.17678 15.7626 9.46967 15.4697L12.9393 12L9.46967 8.53033C9.17678 8.23744 9.17678 7.76256 9.46967 7.46967C9.76256 7.17678 10.2374 7.17678 10.5303 7.46967L14.5303 11.4697Z" fill="currentColor" />
                    </svg>

                </button>
            </div>
        </div>
    </div>

    <div x-ref="swiperContainer" id="carousel-1019211284" class="swiper md:h-full">
        <div class="swiper-wrapper ">
            <div class="swiper-slide !transform-none " data-theme="light" data-hash="index-carousel-1019211284-0">
                <div class="overflow-hidden">
                    <img src="/img/store/store1.png" alt="Magasin ALAIN AFFLELOU" class="w-full object-cover aspect-square rounded-lg" />

                    <div class="p-4">
                        <address class="text-neutral-800 font-medium not-italic mb-2">
                            59 boulevard Jean Jaurès<br>
                            92110, Clichy
                        </address>

                        <div class="flex items-center gap-2 text-sm text-neutral-800">
                            <span class="text-green-600 font-medium">Ouvert</span>
                            <span class="">· Ferme à 19:00</span>
                        </div>
                    </div>
                </div>
            </div>
            <div class="swiper-slide !transform-none " data-theme="light" data-hash="index-carousel-1019211284-1">
                <div class="overflow-hidden">
                    <img src="/img/store/store2.png" alt="Magasin ALAIN AFFLELOU" class="w-full object-cover aspect-square rounded-lg" />

                    <div class="p-4">
                        <address class="text-neutral-800 font-medium not-italic mb-2">
                            59 boulevard Jean Jaurès<br>
                            92110, Clichy
                        </address>

                        <div class="flex items-center gap-2 text-sm text-neutral-800">
                            <span class="text-green-600 font-medium">Ouvert</span>
                            <span class="">· Ferme à 19:00</span>
                        </div>
                    </div>
                </div>
            </div>
            <div class="swiper-slide !transform-none " data-theme="light" data-hash="index-carousel-1019211284-2">
                <div class="overflow-hidden">
                    <img src="/img/store/store3.png" alt="Magasin ALAIN AFFLELOU" class="w-full object-cover aspect-square rounded-lg" />

                    <div class="p-4">
                        <address class="text-neutral-800 font-medium not-italic mb-2">
                            59 boulevard Jean Jaurès<br>
                            92110, Clichy
                        </address>

                        <div class="flex items-center gap-2 text-sm text-neutral-800">
                            <span class="text-green-600 font-medium">Ouvert</span>
                            <span class="">· Ferme à 19:00</span>
                        </div>
                    </div>
                </div>
            </div>
            <div class="swiper-slide !transform-none " data-theme="light" data-hash="index-carousel-1019211284-3">
                <div class="overflow-hidden">
                    <img src="/img/store/store4.png" alt="Magasin ALAIN AFFLELOU" class="w-full object-cover aspect-square rounded-lg" />

                    <div class="p-4">
                        <address class="text-neutral-800 font-medium not-italic mb-2">
                            59 boulevard Jean Jaurès<br>
                            92110, Clichy
                        </address>

                        <div class="flex items-center gap-2 text-sm text-neutral-800">
                            <span class="text-green-600 font-medium">Ouvert</span>
                            <span class="">· Ferme à 19:00</span>
                        </div>
                    </div>
                </div>
            </div>
            <div class="swiper-slide !transform-none " data-theme="light" data-hash="index-carousel-1019211284-4">
                <div class="overflow-hidden">
                    <img src="/img/store/store1.png" alt="Magasin ALAIN AFFLELOU" class="w-full object-cover aspect-square rounded-lg" />

                    <div class="p-4">
                        <address class="text-neutral-800 font-medium not-italic mb-2">
                            59 boulevard Jean Jaurès<br>
                            92110, Clichy
                        </address>

                        <div class="flex items-center gap-2 text-sm text-neutral-800">
                            <span class="text-green-600 font-medium">Ouvert</span>
                            <span class="">· Ferme à 19:00</span>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <div class="flex sm:hidden mt-6">
            <a href="https://www.afflelou.com/" target="_blank" rel="noopenner noreferer" class="mx-auto  btn btn-dark-subtle ">
                Voir toute la collection

            </a>
        </div>
    </div>
</div>

<script>
    function initCarouselcarousel1019211284() {
        return {
            swiper: null,
            options: {
                "color": "dark",
                "slidesPerView": {
                    "mobile": 1.2,
                    "tablet": 3,
                    "desktop": 4
                },
                "spaceBetween": 30,
                "showPagination": false,
                "showNavigation": true,
                "showNavigationMobile": false
            },
            componentId: 'carousel-1019211284',
            init() {
                this.initSwiper();
                if (typeof this.customFunction === 'function') {
                    this.customFunction();
                }
            },
            initSwiper() {
                this.swiper = new Swiper(this.$refs.swiperContainer, {
                    slidesPerView: this.options.slidesPerView.mobile,
                    spaceBetween: this.options.spaceBetween,
                    pagination: this.options.showPagination ? {
                        el: '.swiper-pagination-' + this.componentId,
                        clickable: true,
                        bulletClass: 'bullet',
                        bulletActiveClass: 'active',
                    } : false,
                    navigation: this.options.showNavigation ? {
                        nextEl: '.carousel-button-next-' + this.componentId,
                        prevEl: '.carousel-button-prev-' + this.componentId
                    } : false,
                    hashNavigation: {
                        watchState: true,
                    },
                    breakpoints: {
                        640: {
                            slidesPerView: this.options.slidesPerView.tablet
                        },
                        1024: {
                            slidesPerView: this.options.slidesPerView.desktop
                        }
                    },
                    on: {
                        slideChange: () => this.updateSliderTheme(),
                    },
                });
                this.updateSliderTheme();
            },
            updateSliderTheme() {
                const activeSlide = this.swiper.slides[this.swiper.activeIndex];
                const theme = activeSlide.getAttribute('data-slider-theme');
                this.sliderTheme = theme || 'light';
            },
            customFunction() {
                // Custom function can be defined here and overridden outside.
            }
        };
    }
</script>
{# components/carousel/carousel.twig #}
{% set uniqueId = "carousel-" ~ random() %}
{% set carouselFunctionName = "initCarousel" ~ uniqueId|replace({'-': ''}) %}

<div
        x-data="{{ carouselFunctionName }}()"
        x-init="init()"
        class="relative w-full h-full mx-auto"
>
    {# Header Section #}
    <div class="flex justify-between items-center mb-8 {% if variant is defined and variant == 'small' %} absolute top-6 md:top-10 left-6 md:left-10 right-6 md:right-10 z-10 {% endif %}">
        {% if title is defined and title %}
            <h2 class="text-3xl font-semibold {{ titleClass }}">{{ title }}</h2>
        {% endif %}

        <div class="flex items-center justify-center space-x-4 {% if variant is defined and variant == 'small' %} justify-between w-full {% endif %}">
            {% if variant is defined and variant == 'small' %}
                {% if carousel.showPagination %}
                    <div x-ref="pagination"
                         class="swiper-pagination-{{ uniqueId }} swiper-pagination mt-0 !w-auto {{ paginationClass }}" {{ swiper_pagination_attribute }}></div>
                {% endif %}
            {% else %}
                {% if showCTA %}
                    {% render "@template-button" with {
                        href: CTA.href,
                        external: CTA.external,
                        color: CTA.color ~ "-subtle",
                        label: CTA.label,
                        button_class:"hidden sm:inline-flex mx-auto",
                        button_attribute: cta_button_attribute,
                    } %}
                {% endif %}
            {% endif %}

            {% if carousel.showNavigation %}
                <div class="{% if carousel.showNavigationMobile == false %} hidden  sm:flex {% endif %} flex space-x-2">
                    {% render "@template-button" with {
                        color: carousel.color ~ "-subtle",
                        type: "only-icon",
                        icon: {
                            name: "library--chevron-left"
                        },
                        button_class:"carousel-button-prev-" ~ uniqueId ~ " before:border-none",
                        button_attribute: swiper_navigation_attribute
                    } %}
                    {% render "@template-button" with {
                        color: carousel.color ~ "-subtle",
                        type: "only-icon",
                        icon: {
                            name: "library--chevron-right"
                        },
                        button_class:"carousel-button-next-" ~ uniqueId ~ " before:border-none",
                        button_attribute: swiper_navigation_attribute
                    } %}
                </div>
            {% endif %}
        </div>
    </div>

    <div x-ref="swiperContainer" id="{{ uniqueId }}" class="swiper md:h-full">
        <div class="swiper-wrapper {{ wrapperClass }}">
            {% for index, slide in slides %}
			<div
                        class="swiper-slide !transform-none {{ slideClass }}"
                        data-theme="{{ slide.theme|default('light') }}"
						data-hash="index-{{ uniqueId }}-{{ index }}"
			>
                    {% if slide.template %}
                        {% render "@" ~ slide.template with {
							slide: slide
						} %}
                    {% else %}
                        {{ slide.content|raw }}
                    {% endif %}
                </div>
            {% endfor %}
        </div>

        {% if variant is defined and variant != 'small' and carousel.showPagination %}
            <div x-ref="pagination"
                 class="swiper-pagination-{{ uniqueId }} swiper-pagination {{ paginationClass }}" {{ swiper_pagination_attribute }}></div>
        {% endif %}
        {% if CTA %}
            <div class="flex sm:hidden mt-6">
                {% render "@template-button" with {
                    href: CTA.href,
                    external: CTA.external,
                    color: CTA.color ~ "-subtle",
                    label: CTA.label,
                    button_class:"mx-auto",
                    button_attribute: cta_button_attribute
                } %}
            </div>
        {% endif %}
    </div>
</div>

<script>
	function {{ carouselFunctionName }}() {
		return {
			swiper: null,
			options: {{ carousel|json_encode|raw }},
			componentId: '{{ uniqueId }}',

			init() {
				this.initSwiper();
				if (typeof this.customFunction === 'function') {
					this.customFunction();
				}
			},

			initSwiper() {
				this.swiper = new Swiper(this.$refs.swiperContainer, {
					slidesPerView: this.options.slidesPerView.mobile,
					spaceBetween: this.options.spaceBetween,
					pagination: this.options.showPagination ? {
						el: '.swiper-pagination-' + this.componentId,
						clickable: true,
						bulletClass: 'bullet',
						bulletActiveClass: 'active',
					} : false,
					navigation: this.options.showNavigation ? {
						nextEl: '.carousel-button-next-' + this.componentId,
						prevEl: '.carousel-button-prev-' + this.componentId
					} : false,
					hashNavigation: {
						watchState: true,
					},
					breakpoints: {
						640: {
							slidesPerView: this.options.slidesPerView.tablet
						},
						1024: {
							slidesPerView: this.options.slidesPerView.desktop
						}
					},
					on: {
						slideChange: () => this.updateSliderTheme(),
					},
				});

				this.updateSliderTheme();
			},

			updateSliderTheme() {
				const activeSlide = this.swiper.slides[this.swiper.activeIndex];
				const theme = activeSlide.getAttribute('data-slider-theme');

				this.sliderTheme = theme || 'light';
			},

			customFunction() {
				// Custom function can be defined here and overridden outside.
				{% if customFunction is defined and customFunction %}
					{{ customFunction }}
				{% endif %}
			}
		};
	}
</script>
{
  "title": "Magasins à proximité",
  "variant": "default",
  "showCTA": false,
  "CTA": {
    "label": "Voir toute la collection",
    "href": "https://www.afflelou.com/",
    "external": true,
    "color": "dark"
  },
  "paginationClass": "swiper-light",
  "carousel": {
    "color": "dark",
    "slidesPerView": {
      "mobile": 1.2,
      "tablet": 3,
      "desktop": 4
    },
    "spaceBetween": 30,
    "showPagination": false,
    "showNavigation": true,
    "showNavigationMobile": false
  },
  "slides": [
    {
      "template": "storecard",
      "image": "/img/store/store1.png",
      "name": "ALAIN AFFLELOU",
      "address": {
        "street": "59 boulevard Jean Jaurès",
        "postcode": "92110",
        "city": "Clichy"
      },
      "status": "Ouvert",
      "openUntil": "Ferme à 19:00"
    },
    {
      "template": "storecard",
      "image": "/img/store/store2.png",
      "name": "ALAIN AFFLELOU",
      "address": {
        "street": "59 boulevard Jean Jaurès",
        "postcode": "92110",
        "city": "Clichy"
      },
      "status": "Ouvert",
      "openUntil": "Ferme à 19:00"
    },
    {
      "template": "storecard",
      "image": "/img/store/store3.png",
      "name": "ALAIN AFFLELOU",
      "address": {
        "street": "59 boulevard Jean Jaurès",
        "postcode": "92110",
        "city": "Clichy"
      },
      "status": "Ouvert",
      "openUntil": "Ferme à 19:00"
    },
    {
      "template": "storecard",
      "image": "/img/store/store4.png",
      "name": "ALAIN AFFLELOU",
      "address": {
        "street": "59 boulevard Jean Jaurès",
        "postcode": "92110",
        "city": "Clichy"
      },
      "status": "Ouvert",
      "openUntil": "Ferme à 19:00"
    },
    {
      "template": "storecard",
      "image": "/img/store/store1.png",
      "name": "ALAIN AFFLELOU",
      "address": {
        "street": "59 boulevard Jean Jaurès",
        "postcode": "92110",
        "city": "Clichy"
      },
      "status": "Ouvert",
      "openUntil": "Ferme à 19:00"
    }
  ]
}

Product Card Component

A reusable component for displaying product cards, compatible with both carousels and grids.

Configuration

The component can be configured via the productCard.config.js file. Here are the available options:

Context

Product Properties

Object containing product information:

  • url: Product page URL
  • openInNewTab: Boolean to open link in new tab
  • image: Path to main product image
  • hoverImage: Path to image displayed on hover
  • name: Product name
  • brand: Product brand
  • description: Product description
  • colors: Text indicating available colors
  • price: Product price
  • oldPrice: Original price (for promotions)
  • discountText: Promotion text
  • webLabel: Web price label
  • inlinePrice: Boolean to display price inline with title
  • buttonColor: Action buttons color
  • buttonColorHover: Action buttons hover color

Action Buttons

Action buttons configuration:

  • showActionButtons: Boolean to show/hide action buttons
  • showFavoriteButton: Boolean to show/hide favorite button
  • showQuickViewButton: Boolean to show/hide quick view button

Alpine.js Attributes

Alpine.js attributes configuration for reactivity:

alpineAttribute: {
    productInfo: {
        brand: 'x-text="product.brand"',
        name: 'x-text="product.name"',
        description: 'x-text="product.description"',
        colors: 'x-text="product.colors"',
        price: 'x-text="product.price"',
        webLabel: 'x-text="product.webLabel"'
    }
}

Variants

  1. default: Standard display with image, title, and price
  2. with-hover: Variant with image hover effect
  3. with-discount: Variant with original price and promotion text
  4. inline-price: Variant with price inline with title

Usage

{# Basic usage #}
{% include '@components/product/productCard.twig' with {
    product: {
        url: '/product/1',
        image: '/path/to/image.jpg',
        name: 'Product name',
        price: '199.00'
    }
} %}

{# Usage with all options #}
{% include '@components/product/productCard.twig' with {
    product: {
        url: '/product/1',
        openInNewTab: false,
        image: '/path/to/image.jpg',
        hoverImage: '/path/to/hover-image.jpg',
        name: 'Product name',
        brand: 'Brand',
        description: 'Product description',
        colors: '3 colors available',
        price: '199.00',
        oldPrice: '249.00',
        discountText: '-20%',
        webLabel: 'WEB PRICE',
        inlinePrice: true,
        buttonColor: 'light',
        buttonColorHover: 'dark',
        showActionButtons: true,
        showFavoriteButton: true,
        showQuickViewButton: true,
        addCart: true,
        alpineAttribute: {
            productInfo: {
                brand: 'x-text="product.brand"'
                // ... other Alpine.js attributes
            }
        }
    },
    class: 'custom-class'
} %}

Features

  • Image Hover Effect: Changes image on hover when hoverImage is defined
  • Flexible Pricing: Inline or block display based on inlinePrice
  • Action Buttons: Configurable favorite and quick view buttons
  • Promotions: Support for crossed-out prices and discount percentages
  • Alpine.js: Full reactivity support with Alpine.js
  • Tailwind CSS: Uses Tailwind utility classes for styling
  • Nested Components: Uses template-button component for actions

Technical Details

CSS Classes

The component uses Tailwind CSS classes for styling:

  • Responsive design with flexible grid system
  • Hover effects on images and buttons
  • Customizable spacing and typography
  • Configurable colors through button variants

Template Button Integration

The component integrates with the template-button component for:

  • Favorite button
  • Quick view button
  • Add to cart button Each button can be customized with different colors and hover states.