<!-- Error rendering component -->
<!-- TypeError: Cannot read properties of undefined (reading 'startsWith') -->
<!-- Error: TypeError: Cannot read properties of undefined (reading 'startsWith')
    at /var/www/vhosts/ati4.group/afflelou.design-system.ati4.group/node_modules/@frctl/twig/src/adapter.js:156:24
    at new Promise (<anonymous>)
    at TwigAdapter.render (/var/www/vhosts/ati4.group/afflelou.design-system.ati4.group/node_modules/@frctl/twig/src/adapter.js:134:16)
    at ComponentSource._renderVariant (/var/www/vhosts/ati4.group/afflelou.design-system.ati4.group/node_modules/@frctl/fractal/src/api/components/source.js:212:30)
    at _renderVariant.next (<anonymous>)
    at onFulfilled (/var/www/vhosts/ati4.group/afflelou.design-system.ati4.group/node_modules/co/index.js:65:19) -->
{% set uniqueId = "carousel-" ~ random() %}
{% set carouselFunctionName = "initCarousel" ~ uniqueId|replace({'-': ''}) %}

<div x-data="{{ carouselFunctionName }}()" x-init="init()" class="relative w-full max-w-7xl mx-auto">
	<div x-ref="swiperContainer" id="{{ uniqueId }}" class="swiper lg:h-full">
		<div class="swiper-wrapper lg:!flex lg:!transform-none lg:flex-wrap lg:gap-4">
			{% if slides is defined and slides %}
				{% for index, product in slides %}
					{# Utilisation de style display block par défaut pour les 3 premiers éléments #}
					<div data-hash="index-{{ uniqueId }}-{{ index }}"
						 style="{% if index < 3 %}display: block;{% endif %}"
						 x-cloak
						 x-show="isVisible({{ index }})"
						 x-transition:enter="transition-opacity duration-500"
						 x-transition:enter-start="opacity-0"
						 x-transition:enter-end="opacity-100"
						 x-transition:leave="transition-opacity duration-300"
						 x-transition:leave-start="opacity-100"
						 x-transition:leave-end="opacity-0"
						 class="swiper-slide relative overflow-hidden lg:!mr-0 {% if loop.first or index == 5 or index == 6 %} lg:!w-full {% else %} lg:!w-[calc(50%_-_8px)] {% endif %}">
						<img src="{{ product.src|path }}"
							 alt="{{ product.alt }}"
							 class="w-full h-full object-cover aspect-square">
						{% if loop.first %}
							<div class="absolute bottom-4 hidden lg:flex gap-2 w-full justify-center">
								{% render "@template-button" with {
									color: "light",
									size: "sm",
									type: "leading-icon",
									label: "Essai virtuel",
									icon: {
										name: "library--camera"
									}
								} %}
								{% render "@template-button" with {
									color: "light",
									size: "sm",
									type: "leading-icon",
									label: "Vue 3D",
									icon: {
										name: "library--3d"
									}
								} %}
							</div>
						{% endif %}
					</div>
				{% endfor %}
			{% endif %}
		</div>

		{% if carousel.showPagination %}
			<div x-ref="pagination"
				 class="swiper-pagination-{{ uniqueId }} swiper-pagination lg:hidden {{ paginationClass }}"
					{{ swiper_pagination_attribute }}></div>
		{% endif %}
	</div>

	<div x-ref="pdpButtonProductGallerie" class="hidden lg:flex gap-2 w-full justify-center mt-4">
		{% render "@template-button" with {
			color: "dark-ghost",
			label: "Voir plus",
			button_attribute: 'x-show="visibleItems < maxItems" @click="showMore"'
		} %}
		{% render "@template-button" with {
			color: "dark-ghost",
			label: "Voir moins",
			button_attribute: 'x-show="visibleItems > 3" @click="showLess"'
		} %}
	</div>
</div>

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

			showMore() {
				this.visibleItems = Math.min(this.visibleItems + 3, this.maxItems);
				this.scrollToLastVisible();
			},

			showLess() {
				this.visibleItems = Math.max(3, this.visibleItems - 3);
				this.scrollToLastVisible();
			},

			scrollToLastVisible() {
				// Attendre que le DOM soit mis à jour
				setTimeout(() => {
					const lastVisibleItem = document.querySelector(`[data-hash="index-${this.componentId}-${this.visibleItems - 1}"]`);
					if (lastVisibleItem) {
						lastVisibleItem.scrollIntoView({ behavior: 'smooth', block: "end"});
					}
				}, 100);
			},

			isVisible(index) {
				return this.$store.screen.isTablet || index < this.visibleItems;
			},

			init() {
				this.$watch('$store.screen.isTablet', (isTablet) => {
					if (isTablet && !this.swiper) {
						this.initSwiper();
					} else if (!isTablet && this.swiper) {
						this.swiper.destroy(true, true);
						this.swiper = null;
					}
				});

				if (this.$store.screen.isTablet) {
					this.initSwiper();
				}
			},

			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
						}
					}
				});
			},
		};
	}
</script>
{
  "title": "La collection MAGIC",
  "variant": "default",
  "showCTA": true,
  "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": 2.2,
      "desktop": 3
    },
    "spaceBetween": 30,
    "showPagination": true,
    "showNavigation": true,
    "showNavigationMobile": false
  },
  "slides": [
    {
      "template": "productcard",
      "brand": "ALAIN AFFLELOU",
      "name": "MAGIC 262",
      "description": "Lunettes de vue MAGIC + 2 clips inclus",
      "colors": "3 couleurs disponibles",
      "price": "199,00",
      "image": "/img/productsSlider/magicProduct1.png",
      "hoverImage": "/img/productsSlider/hoverMagicProduct1.png",
      "webLabel": "PRIX WEB"
    },
    {
      "template": "productcard",
      "brand": "ALAIN AFFLELOU",
      "name": "Enola",
      "description": "Lunettes de vue MAGIC",
      "colors": "5 couleurs disponibles",
      "price": "109,00",
      "image": "/img/productsSlider/magicProduct2.png",
      "hoverImage": "/img/productsSlider/hoverMagicProduct2.png",
      "webLabel": "PRIX WEB"
    },
    {
      "template": "productcard",
      "brand": "ALAIN AFFLELOU",
      "name": "MAGIC 222",
      "description": "Lunettes de vue MAGIC + 2 clips inclus",
      "colors": "2 couleurs disponibles",
      "price": "179,00",
      "image": "/img/productsSlider/magicProduct3.png",
      "hoverImage": "/img/productsSlider/hoverMagicProduct3.png",
      "webLabel": "PRIX WEB"
    },
    {
      "template": "productcard",
      "brand": "ALAIN AFFLELOU",
      "name": "Enola",
      "description": "Lunettes de vue MAGIC",
      "colors": "5 couleurs disponibles",
      "price": "109,00",
      "image": "/img/productsSlider/magicProduct1.png",
      "hoverImage": "/img/productsSlider/hoverMagicProduct1.png",
      "webLabel": "PRIX WEB"
    },
    {
      "template": "productcard",
      "brand": "ALAIN AFFLELOU",
      "name": "MAGIC 222",
      "description": "Lunettes de vue MAGIC + 2 clips inclus",
      "colors": "2 couleurs disponibles",
      "price": "179,00",
      "image": "/img/productsSlider/magicProduct2.png",
      "hoverImage": "/img/productsSlider/hoverMagicProduct2.png",
      "webLabel": "PRIX WEB"
    }
  ]
}

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.