{% extends 'front/base.html.twig' %}
{% set company = Globals.getCompany() %}
{% block meta_description %}
{{ websiteSettingService.get('description', '')|striptags|trim }}
{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('front/assets/css/product-card.css') }}">
<link rel="stylesheet" href="{{ asset('front/assets/css/home.css') }}">
{% endblock %}
{% block body %}
{% set heroSlides = sliders|default([])|slice(0,3) %}
{% set showHomepageHero = websiteSettingService.get('showHomepageHero', true) %}
<main class="main pt-4 pb-5" style="background-color: #fff; color: #222121;">
{% if showHomepageHero and heroSlides|length %}
<section class="hero-slider-section hero-slider--bleed" aria-label="Slider principal">
<div class="hero-slider">
<div class="hero-slider-track d-flex" style="transition:transform .5s ease;" aria-live="polite">
{% for slider in heroSlides %}
{% set mode = slider.displayMode|default('full') %}
<article
class="hero-slide hero-slide--{{ mode }} flex-shrink-0"
style="{% if mode == 'full' %}background-image:url('{{ asset(slider.image) }}');{% endif %}"
>
<div class="hero-slide-gradient" aria-hidden="true"></div>
{% if mode == 'full' %}
<div class="hero-slide-inner">
<div class="hero-slide-content">
<span class="hero-slide-kicker">{{ company.name|default('Collection') }}</span>
{% if slider.title %}
<h2 class="hero-slide-title">{{ slider.title }}</h2>
{% endif %}
{% if slider.description %}
<p class="hero-slide-subtitle">{{ slider.description|striptags }}</p>
{% endif %}
{% if slider.url %}
<a class="btn hero-slide-cta btn-light btn-lg mt-3" href="{{ slider.url }}">
{{ slider.ctaLabel ?: 'Découvrir' }}
</a>
{% endif %}
</div>
</div>
{% else %}
<div class="hero-asset-layout">
<div class="hero-asset-media">
<img src="{{ asset(slider.image) }}" alt="{{ slider.title ?: 'Slide' }}">
</div>
<div class="hero-asset-content">
<span class="hero-slide-kicker">{{ company.name|default('Collection') }}</span>
{% if slider.title %}
<h2 class="hero-slide-title">{{ slider.title }}</h2>
{% endif %}
{% if slider.description %}
<p class="hero-slide-subtitle">{{ slider.description|striptags }}</p>
{% endif %}
{% if slider.url %}
<a class="btn hero-slide-cta btn-light btn-lg mt-3" href="{{ slider.url }}">
{{ slider.ctaLabel ?: 'Découvrir' }}
</a>
{% endif %}
</div>
</div>
{% endif %}
</article>
{% endfor %}
</div>
<button type="button" class="hero-slider-arrow hero-slider-arrow--prev" aria-label="Slide precedente">
<span aria-hidden="true">‹</span>
</button>
<button type="button" class="hero-slider-arrow hero-slider-arrow--next" aria-label="Slide suivante">
<span aria-hidden="true">›</span>
</button>
<div class="hero-slider-dots" role="tablist" aria-label="Indicateurs du slider"></div>
</div>
</section>
{% endif %}
{# Barre descriptive supprimée : le slider gère tout #}
{% set homepageMode = websiteSettingService.get('homepageMode')|default('category') %}
{% set homepageColumns = websiteSettingService.get('homepageColumns')|default(2) %}
{% set homepageOrder = websiteSettingService.get('homepageOrder')|default(6) %}
{% set homepageProductsPerTab = websiteSettingService.get('homepageProductsPerTab')|default(12) %}
{% set showHomepageTopRated = websiteSettingService.get('showHomepageTopRated', true) %}
{% set showHomepagePacks = websiteSettingService.get('showHomepagePacks', true) %}
{% set catalogListMode = websiteSettingService.get('catalogListMode')|default('product') %}
<script>
var HOME_DEFAULT_MODE = '{{ homepageMode }}';
var HOME_DEFAULT_COLUMNS = {{ homepageColumns }};
var HOME_DEFAULT_ORDER = {{ homepageOrder }};
var HOME_PRODUCTS_PER_TAB = {{ homepageProductsPerTab }};
var HOME_CATALOG_LIST_MODE = '{{ catalogListMode }}';
var HOME_SHOW_TOP_RATED = {{ showHomepageTopRated ? 'true' : 'false' }};
var HOME_SHOW_PACKS = {{ showHomepagePacks ? 'true' : 'false' }};
var HOME_MODE_LABELS = {{ { all: 'Tous',
category: websiteSettingService.get('homepageLabelCategory', 'Catégories'),
new: websiteSettingService.get('homepageLabelNew', 'Nouveautés'),
promo: websiteSettingService.get('homepageLabelPromo', 'Promos'),
top: websiteSettingService.get('homepageLabelTop', 'Top ventes'),
topRated: websiteSettingService.get('homepageLabelTopRated', 'Top classé'),
packs: websiteSettingService.get('homepageLabelPacks', 'Nos Packs')
}|json_encode|raw }};
var HOMEPAGE_FALLBACK_PRODUCTS = {{ products|json_encode|raw }};
var HOME_SHOW_OUT_OF_STOCK = {{ websiteSettingService.get('showOutOfStock')|default(false)|json_encode|raw }};
</script>
<div id="app">
<section id="mode-display" class="container">
<div class="mode-controls">
<div class="mode-toggle" role="tablist" aria-label="Modes d'affichage accueil">
{# Mode tous #}
<button class="mode-item"
:class="{ active: activeMode === 'all' }"
@click="setMode('all')">
${ getModeLabel('all') }
</button>
{# Mode catégorie #}
<button class="mode-item"
:class="{ active: activeMode === 'category' }"
@click="setMode('category')">
${ getModeLabel('category') }
</button>
{# Mode nouveautés #}
<button class="mode-item"
:class="{ active: activeMode === 'new' }"
@click="setMode('new')">
${ getModeLabel('new') }
</button>
{# Mode promos #}
<button class="mode-item"
:class="{ active: activeMode === 'promo' }"
@click="setMode('promo')">
${ getModeLabel('promo') }
</button>
{# Mode top ventes #}
<button class="mode-item"
:class="{ active: activeMode === 'top' }"
@click="setMode('top')">
${ getModeLabel('top') }
</button>
{# Mode top classé #}
<button class="mode-item"
v-if="isHomeModeEnabled('topRated')"
:class="{ active: activeMode === 'topRated' }"
@click="setMode('topRated')">
${ getModeLabel('topRated') }
</button>
{# Mode packs #}
<button class="mode-item"
v-if="isHomeModeEnabled('packs')"
:class="{ active: activeMode === 'packs' }"
@click="setMode('packs')">
${ getModeLabel('packs') }
</button>
</div>
<div class="view-controls" aria-label="Options d'affichage">
{# Filtre — icône + texte #}
<button class="filter-trigger"
type="button"
data-bs-toggle="modal"
data-bs-target="#filterModal"
data-filter-source="home">
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
<path d="M3 5h18M6 12h12M10 19h4"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"/>
</svg>
<span>Filtre</span>
</button>
{# Séparateur #}
<span class="view-separator" aria-hidden="true"></span>
<div class="view-density" aria-label="Nombre de colonnes">
{# Vue 1 colonne #}
<button class="view-btn"
type="button"
:class="{ active: homeColumns === 1 }"
@click="setColumns(1)"
title="1 colonne"
aria-label="1 colonne">
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
<rect x="5" y="3" width="14" height="18" rx="2"
fill="currentColor"/>
</svg>
</button>
{# Vue 2 colonnes #}
<button class="view-btn"
type="button"
:class="{ active: homeColumns === 2 }"
@click="setColumns(2)"
title="2 colonnes"
aria-label="2 colonnes">
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
<rect x="3" y="3" width="8" height="18" rx="2"
fill="currentColor"/>
<rect x="13" y="3" width="8" height="18" rx="2"
fill="currentColor"/>
</svg>
</button>
{# Vue 3 colonnes #}
<button class="view-btn"
type="button"
:class="{ active: homeColumns === 3 }"
@click="setColumns(3)"
title="3 colonnes"
aria-label="3 colonnes">
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
<rect x="2" y="3" width="6" height="18" rx="2"
fill="currentColor"/>
<rect x="9" y="3" width="6" height="18" rx="2"
fill="currentColor"/>
<rect x="16" y="3" width="6" height="18" rx="2"
fill="currentColor"/>
</svg>
</button>
{# Vue 4 colonnes #}
<button class="view-btn"
type="button"
:class="{ active: homeColumns === 4 }"
@click="setColumns(4)"
title="4 colonnes"
aria-label="4 colonnes">
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
<rect x="2" y="3" width="4" height="18" rx="1.5"
fill="currentColor"/>
<rect x="7" y="3" width="4" height="18" rx="1.5"
fill="currentColor"/>
<rect x="12" y="3" width="4" height="18" rx="1.5"
fill="currentColor"/>
<rect x="17" y="3" width="4" height="18" rx="1.5"
fill="currentColor"/>
</svg>
</button>
</div>
</div>
</div>
<div class="active-filters-row" v-if="hasActiveFilters">
<span class="active-filter-chip" v-for="badge in activeFilterBadges" :key="badge.type + '-' + badge.value">
${ badge.label }
<button type="button" @click="clearHomeFilterBadge(badge)" aria-label="Supprimer ce filtre">×</button>
</span>
</div>
<div class="active-filters-meta" v-if="hasActiveFilters && ['new', 'promo', 'top', 'topRated'].indexOf(activeMode) !== -1">
${ currentResultsCount } résultats correspondent aux filtres actifs.
</div>
<div class="home-empty" v-if="homeEmptyMessage">
{# Icône information #}
<span class="home-empty-icon" aria-hidden="true">
<i class="fa fa-archive"></i>
</span>
{# Texte #}
<span class="home-empty-text" v-text="homeEmptyMessage"></span>
</div>
</section>
<section v-if="activeMode==='all'" class="container home-section">
<div v-if="loadingMode==='all' && !homeAllProducts.length" class="row g-3">
<div :class="getColClass()" v-for="n in 4" :key="'sk-all-'+n">
{% include 'front/_product_skeleton.html.twig' %}
</div>
</div>
<div v-if="loadingMode==='all' && !homeAllProducts.length" class="text-center text-muted my-4">
Chargement des produits...
</div>
<div v-else class="row g-3">
<div :class="getColClass()" v-for="prod in homeAllProducts" :key="prod.id">
{% include 'front/_product_card.html.twig' with { var: 'prod' } %}
</div>
<div :class="getColClass()" v-if="loadingMoreAll" v-for="n in 2" :key="'sk-all-more-'+n">
{% include 'front/_product_skeleton.html.twig' %}
</div>
</div>
<div class="d-flex justify-content-center mt-3">
<button class="btn btn-sm btn-dark"
v-if="homeAllHasMore"
:disabled="loadingMoreAll"
@click="loadMoreAll">
${ loadingMoreAll ? 'Chargement...' : 'Voir plus' }
</button>
</div>
</section>
<section v-else-if="activeMode==='category'" class="container home-section">
<template v-if="loadingMode==='category' || !homeCategoryRenderReady">
<div class="row g-3">
<div :class="getColClass()" v-for="n in 4" :key="'sk-cat-'+n">
{% include 'front/_product_skeleton.html.twig' %}
</div>
</div>
<div class="text-center text-muted my-4">
Chargement des visuels en cours...
</div>
</template>
<template v-else>
<div v-for="entry in homeCategories" :key="entry.category.id">
<div class="section-header">
<h2 class="section-title" v-text="entry.category.name"></h2>
</div>
<div v-if="entry.loaded" class="row g-3">
<div :class="getColClass()" v-for="prod in entry.products.slice(0, getHomeProductsLimit())" :key="prod.id">
{% include 'front/_product_card.html.twig' with { var: 'prod' } %}
</div>
</div>
<div v-else class="row g-3">
<div :class="getColClass()" v-for="n in Math.min(getHomeProductsLimit(), 4)" :key="'sk-cat-entry-' + entry.category.id + '-' + n">
{% include 'front/_product_skeleton.html.twig' %}
</div>
</div>
<div class="d-flex justify-content-center mt-3 mb-2">
<button class="btn btn-dark px-4 py-2" style="min-width: 180px;" @click="openCategory(entry.category.id, entry.category.name)">Voir plus</button>
</div>
</div>
</template>
</section>
<section v-else-if="activeMode==='new'" class="container home-section">
<div v-if="loadingMode==='new' && !homeNewProducts.length" class="row g-3">
<div :class="getColClass()" v-for="n in 4" :key="'sk-new-'+n">
{% include 'front/_product_skeleton.html.twig' %}
</div>
</div>
<div v-if="loadingMode==='new' && !homeNewProducts.length" class="text-center text-muted my-4">
Chargement des nouveautés...
</div>
<div v-else class="row g-3">
<div :class="getColClass()" v-for="prod in homeNewProducts" :key="prod.id">
{% include 'front/_product_card.html.twig' with { var: 'prod' } %}
</div>
<div :class="getColClass()" v-if="loadingMoreNew" v-for="n in 2" :key="'sk-new-more-'+n">
{% include 'front/_product_skeleton.html.twig' %}
</div>
</div>
<div class="d-flex justify-content-center mt-3">
<button class="btn btn-sm btn-dark"
v-if="homeNewHasMore"
:disabled="loadingMoreNew"
@click="loadMoreNew">
${ loadingMoreNew ? 'Chargement...' : 'Voir plus' }
</button>
</div>
</section>
<section v-else-if="activeMode==='promo'" class="container home-section">
<div v-if="loadingMode==='promo' && !homePromoProducts.length" class="row g-3">
<div :class="getColClass()" v-for="n in 4" :key="'sk-promo-'+n">
{% include 'front/_product_skeleton.html.twig' %}
</div>
</div>
<div v-if="loadingMode==='promo' && !homePromoProducts.length" class="text-center text-muted my-4">
Chargement des promotions...
</div>
<div v-else class="row g-3">
<div :class="getColClass()" v-for="prod in homePromoProducts" :key="prod.id">
{% include 'front/_product_card.html.twig' with { var: 'prod' } %}
</div>
<div :class="getColClass()" v-if="loadingMorePromo" v-for="n in 2" :key="'sk-promo-more-'+n">
{% include 'front/_product_skeleton.html.twig' %}
</div>
</div>
<div class="d-flex justify-content-center mt-3">
<button class="btn btn-sm btn-dark"
v-if="homePromoHasMore"
:disabled="loadingMorePromo"
@click="loadMorePromo">
${ loadingMorePromo ? 'Chargement...' : 'Voir plus' }
</button>
</div>
</section>
<section v-else-if="activeMode==='top'" class="container home-section">
<div v-if="loadingMode==='top' && !homeTopProducts.length" class="row g-3">
<div :class="getColClass()" v-for="n in 4" :key="'sk-top-'+n">
{% include 'front/_product_skeleton.html.twig' %}
</div>
</div>
<div v-if="loadingMode==='top' && !homeTopProducts.length" class="text-center text-muted my-4">
Chargement des best-sellers...
</div>
<div v-else class="row g-3">
<div :class="getColClass()" v-for="prod in homeTopProducts" :key="prod.id">
{% include 'front/_product_card.html.twig' with { var: 'prod' } %}
</div>
<div :class="getColClass()" v-if="loadingMoreTop" v-for="n in 2" :key="'sk-top-more-'+n">
{% include 'front/_product_skeleton.html.twig' %}
</div>
</div>
<div class="d-flex justify-content-center mt-3">
<button class="btn btn-sm btn-dark"
v-if="homeTopHasMore"
:disabled="loadingMoreTop"
@click="loadMoreTop">
${ loadingMoreTop ? 'Chargement...' : 'Voir plus' }
</button>
</div>
</section>
<section v-else-if="activeMode==='topRated'" class="container home-section">
<div v-if="loadingMode==='topRated' && !homeTopRatedProducts.length" class="row g-3">
<div :class="getColClass()" v-for="n in 4" :key="'sk-top-rated-'+n">
<div class="card-product skeleton-card">
<div class="skeleton-image"></div>
<div class="card-product-body">
<div class="skeleton-line w-80"></div>
<div class="skeleton-line w-60"></div>
<div class="skeleton-line w-40"></div>
</div>
</div>
</div>
</div>
<div v-else class="row g-3">
<div :class="getColClass()" v-for="prod in homeTopRatedProducts.slice(0, getHomeProductsLimit())" :key="prod.id">
{% include 'front/_product_card.html.twig' with { var: 'prod' } %}
</div>
</div>
</section>
<section v-else-if="activeMode==='packs'" class="container home-section">
<div v-if="loadingMode==='packs' && !homePacks.length" class="row g-3">
<div :class="getColClass()" v-for="n in 4" :key="'sk-pack-'+n">
{% include 'front/_product_skeleton.html.twig' %}
</div>
</div>
<div v-if="loadingMode==='packs' && !homePacks.length" class="text-center text-muted my-4">
Chargement des packs...
</div>
<div v-else class="row g-3">
<div :class="getColClass()" v-for="p in homePacks.slice(0, getHomeProductsLimit())" :key="p.id">
<div class="card-product">
<div class="card-product-media" style="--img-aspect: 0.8;">
<img class="is-loaded"
:src="packImgUrl(p.picture)"
:alt="p.name"
@click="goToPackConfig(p)"
style="cursor:pointer;">
<h3 class="home-card-title product-name">${ p.name }</h3>
</div>
<div class="card-product-body">
<div class="small text-muted mb-1">${ p.reference || '' }</div>
<div class="product-price">
<div class="price-current">
<span class="price-value">${ formatPrice(p.final_price_ttc) } TND</span>
<span class="price-meta" v-if="Number(p.remise||0) > 0">
<span class="original-price">${ formatPrice(p.price_ttc) } TND</span>
<span class="discount-flag">-${ Number(p.remise||0).toFixed(2) }%</span>
</div>
</div>
</div>
<button class="btn btn-dark btn-sm mt-2" @click="goToPackConfig(p)">
Configurer ce pack
</button>
</div>
</div>
</div>
</div>
</section>
</div>
</main>
<section class="info-icons-wrapper container" aria-label="Services et garanties">
<header class="info-icons-header text-center">
<h2 class="info-icons-title">Nos services</h2>
<p class="info-icons-subtitle">Tout ce qu’il faut pour une expérience d’achat simple et sereine.</p>
</header>
<div class="info-icons-separator" aria-hidden="true"></div>
<div class="info-icons">
<a href="{{ path('delivery_information') }}" class="info-item">
<img src="{{ asset('images/icons/livraison.png') }}" alt="Livraison rapide">
<span>Livraison rapide</span>
</a>
<a href="{{ path('return_and_exchange') }}" class="info-item">
<img src="{{ asset('images/icons/retour.png') }}" alt="Retours faciles">
<span>Retours faciles</span>
</a>
<a href="{{ path('terms_of_sales') }}" class="info-item">
<img src="{{ asset('images/icons/paiement.png') }}" alt="Paiement sécurisé">
<span>Paiement sécurisé</span>
</a>
<a href="{{ path('contact') }}" class="info-item">
<img src="{{ asset('images/icons/contact.png') }}" alt="Support client">
<span>Support client</span>
</a>
</div>
</section>
{% include 'front/product/_filter_modal.html.twig' %}
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script>
document.addEventListener('DOMContentLoaded', function () {
var slider = document.querySelector('.hero-slider');
if (!slider) {
return;
}
var wrapper = slider.querySelector('.hero-slider-track');
var slides = slider.querySelectorAll('.hero-slide');
if (!slides.length) {
return;
}
var currentIndex = 0;
var leftArrow = slider.querySelector('.hero-slider-arrow--prev');
var rightArrow = slider.querySelector('.hero-slider-arrow--next');
var dotsWrapper = slider.querySelector('.hero-slider-dots');
var prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var dots = [];
var setSlide = (index) => {
currentIndex = (index + slides.length) % slides.length;
wrapper.style.transform = `translate3d(-${currentIndex * 100}%, 0, 0)`;
updateDots();
};
var updateDots = () => {
dots.forEach((dot, idx) => {
dot.classList.toggle('active', idx === currentIndex);
dot.setAttribute('aria-selected', idx === currentIndex);
});
};
if (dotsWrapper) {
slides.forEach((_, idx) => {
var dot = document.createElement('button');
dot.type = 'button';
dot.className = 'hero-slider-dot';
dot.setAttribute('role', 'tab');
dot.setAttribute('aria-label', `Slide ${idx + 1}`);
dot.addEventListener('click', () => {
setSlide(idx);
resetAutoplay();
});
dotsWrapper.appendChild(dot);
dots.push(dot);
});
}
var goToPrevious = () => {
setSlide(currentIndex - 1);
resetAutoplay();
};
var goToNext = () => {
setSlide(currentIndex + 1);
resetAutoplay();
};
if (leftArrow) {
leftArrow.addEventListener('click', goToPrevious);
}
if (rightArrow) {
rightArrow.addEventListener('click', goToNext);
}
setSlide(currentIndex);
var autoplayId = null;
var startAutoplay = () => {
if (slides.length <= 1) {
return;
}
if (prefersReduced) {
return;
}
autoplayId = setInterval(goToNext, 5000);
};
var resetAutoplay = () => {
if (autoplayId) {
clearInterval(autoplayId);
autoplayId = null;
}
startAutoplay();
};
slider.addEventListener('mouseenter', function () {
if (autoplayId) {
clearInterval(autoplayId);
autoplayId = null;
}
});
slider.addEventListener('mouseleave', function () {
startAutoplay();
});
startAutoplay();
});
</script>
<script src="{{ asset('front/assets/scripts/home/home.js') }}"></script>
{% endblock %}