<!-- 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"
}
]
}
A reusable component for displaying product cards, compatible with both carousels and grids.
The component can be configured via the productCard.config.js
file. Here are the available options:
Object containing product information:
url
: Product page URLopenInNewTab
: Boolean to open link in new tabimage
: Path to main product imagehoverImage
: Path to image displayed on hovername
: Product namebrand
: Product branddescription
: Product descriptioncolors
: Text indicating available colorsprice
: Product priceoldPrice
: Original price (for promotions)discountText
: Promotion textwebLabel
: Web price labelinlinePrice
: Boolean to display price inline with titlebuttonColor
: Action buttons colorbuttonColorHover
: Action buttons hover colorAction buttons configuration:
showActionButtons
: Boolean to show/hide action buttonsshowFavoriteButton
: Boolean to show/hide favorite buttonshowQuickViewButton
: Boolean to show/hide quick view buttonAlpine.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"'
}
}
default
: Standard display with image, title, and pricewith-hover
: Variant with image hover effectwith-discount
: Variant with original price and promotion textinline-price
: Variant with price inline with title{# 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'
} %}
hoverImage
is definedinlinePrice
template-button
component for actionsThe component uses Tailwind CSS classes for styling:
The component integrates with the template-button component for: