{% extends 'base_home.html.twig' %}
{% block title %}
{{ product.name }}
| MaketOu
{% endblock %}
{% block stylesheets %}
<style>
/* Styles pour l'affichage de la boutique */
.shop-info {
padding: 10px 0;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 15px;
}
.shop-link {
color: #ffa200;
text-decoration: none;
font-weight: 500;
}
.shop-link:hover {
color: #e8910a;
text-decoration: underline;
}
/* Styles pour les statistiques du produit */
.product-stats {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.product-stats .stat-item {
text-align: center;
}
.product-stats .stat-number {
display: block;
font-size: 1.2rem;
font-weight: bold;
color: #ffa200;
}
.product-stats .stat-label {
color: #666;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Message de confirmation */
#cart-message {
border: none;
background: #d4edda;
color: #155724;
border-radius: 6px;
padding: 12px 16px;
margin-top: 15px;
}
/* Amélioration des boutons d'actions */
.card_area {
margin-top: 20px;
gap: 12px;
}
.card_area .primary-btn {
background: #ffa200;
border: none;
padding: 0 30px;
height: 50px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 8px;
color: white;
font-weight: 600;
font-size: 16px;
transition: all 0.3s ease;
text-decoration: none;
gap: 8px;
flex: 1;
max-width: 250px;
}
.card_area .primary-btn i {
font-size: 20px;
}
.card_area .primary-btn:hover {
background: #e8910a;
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(255, 162, 0, 0.3);
color: white;
}
.card_area .icon_btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
border-radius: 8px;
background: #fff;
color: #333;
border: 1px solid #ddd;
transition: all 0.3s ease;
text-decoration: none;
}
.card_area .icon_btn i {
font-size: 22px;
}
.card_area .icon_btn:hover {
background: #ffa200;
color: white;
border-color: #ffa200;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 162, 0, 0.2);
}
/* Styles pour la galerie moderne de produits */
.product-gallery-modern {
display: flex;
gap: 15px;
position: relative;
align-items: stretch;
}
.thumbnails-container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 80px;
flex-shrink: 0;
height: auto;
}
/* Les boutons sont toujours visibles (meilleur pour le mobile et l'UX) */
.thumbnails-container .thumbnail-nav-btn {
transition: all 0.2s ease;
}
.product-thumbnails {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
flex: 1;
overflow: hidden;
position: relative;
}
.thumbnails-wrapper {
display: flex;
flex-direction: column;
gap: 10px;
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
}
.thumbnail-nav-btn {
width: 30px;
height: 30px;
border-radius: 50%;
background: white;
border: 2px solid #e0e0e0;
display: flex !important;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
margin: 0;
padding: 0;
color: #666;
z-index: 10;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
position: relative;
flex-shrink: 0;
}
.thumbnail-nav-btn:hover {
background: #ffa200;
border-color: #ffa200;
color: white;
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(255, 162, 0, 0.3);
}
.thumbnail-nav-btn:active {
transform: scale(0.95);
}
.thumbnail-nav-btn.disabled {
opacity: 0.3;
cursor: not-allowed;
pointer-events: none;
filter: grayscale(1);
}
.thumbnail-nav-btn i {
font-size: 20px;
font-weight: bold;
}
.thumbnail-item {
width: 80px;
height: 80px;
min-width: 80px;
min-height: 80px;
max-width: 80px;
max-height: 80px;
border: 2px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: border-color 0.3s ease, border-width 0.3s ease, box-shadow 0.3s ease;
position: relative;
background: #fff;
border-bottom: 2px solid #e0e0e0;
flex-shrink: 0;
}
.thumbnail-item:hover {
border-color: #ffa200;
border-bottom-color: #ffa200;
border-width: 3px;
box-shadow: 0 0 0 2px rgba(255, 162, 0, 0.2);
}
.thumbnail-item.active {
border-color: #ffa200 !important;
border-bottom-color: #ffa200 !important;
border-width: 3px !important;
box-shadow: 0 0 0 2px rgba(255, 162, 0, 0.2) !important;
}
.thumbnail-item::after,
.thumbnail-item::before {
display: none !important;
content: none !important;
}
.thumbnail-item img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.thumbnail-item:hover img {
transform: scale(1.1);
}
.thumbnail-item.active img {
transform: scale(1.1);
}
.product-main-image-wrapper {
flex: 1;
position: relative;
background: #fff;
border-radius: 12px;
overflow: hidden;
border: 1px solid #e0e0e0;
cursor: crosshair;
}
.product-main-image {
position: relative;
width: 100%;
padding-top: 100%;
overflow: hidden;
background: #f8f9fa;
}
.product-main-image img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
transition: transform 0.15s ease-out;
transform-origin: center center;
}
/* Supprimer le scale basique, géré en JS */
/* .product-main-image:hover img {
transform: scale(1.05);
} */
.image-overlay-icons {
position: absolute;
top: 15px;
right: 15px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 20;
pointer-events: none;
}
.icon-btn {
pointer-events: all;
width: 45px;
height: 45px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.95);
border: none;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
color: #333;
}
.icon-btn:hover {
background: #ffa200;
color: white;
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(255, 162, 0, 0.4);
}
.icon-btn i {
font-size: 18px;
}
.favorite-btn.active {
background: #ff6b6b;
color: white;
}
.image-counter {
position: absolute;
bottom: 15px;
right: 15px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 6px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
}
/* Modal de zoom d'image avec transitions modernes */
.image-zoom-modal {
display: none;
position: fixed;
z-index: 9999;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0);
overflow: auto;
opacity: 0;
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), background 0.3s ease;
backdrop-filter: blur(0px);
}
.image-zoom-modal.active {
display: flex;
align-items: center;
justify-content: center;
opacity: 1;
background: rgba(0, 0, 0, 0.95);
backdrop-filter: blur(10px);
}
.zoom-modal-content {
position: relative;
max-width: 90%;
max-height: 90%;
margin: auto;
opacity: 0;
transform: scale(0.8);
transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.image-zoom-modal.active .zoom-modal-content {
opacity: 1;
transform: scale(1);
}
.zoom-modal-content img {
width: 100%;
height: auto;
max-height: 90vh;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 0.3s ease;
}
.zoom-modal-content img.loaded {
opacity: 1;
}
/* Loader moderne */
.zoom-loader {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.zoom-loader-spinner {
width: 60px;
height: 60px;
border: 4px solid rgba(255, 255, 255, 0.1);
border-top-color: #ffa200;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.zoom-loader-text {
color: white;
margin-top: 15px;
text-align: center;
font-size: 14px;
font-weight: 500;
}
.close-zoom {
position: absolute;
top: 30px;
right: 30px;
color: white;
font-size: 32px;
font-weight: 300;
cursor: pointer;
z-index: 10000;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
opacity: 0;
transform: scale(0.8);
}
.image-zoom-modal.active .close-zoom {
opacity: 1;
transform: scale(1);
transition-delay: 0.2s;
}
.close-zoom:hover {
background: rgba(255, 162, 0, 0.9);
border-color: #ffa200;
transform: scale(1.1) rotate(90deg);
box-shadow: 0 4px 20px rgba(255, 162, 0, 0.4);
}
/* Boutons de navigation sur l'image principale */
.main-image-nav {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
pointer-events: none;
z-index: 15;
}
.main-image-nav-btn {
pointer-events: all;
width: 44px;
height: 44px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.95);
border: 2px solid #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
color: #333;
transition: all 0.2s ease;
}
.main-image-nav-btn:hover {
background: #ffa200;
border-color: #ffa200;
color: #fff;
transform: scale(1.08);
}
.main-image-nav-btn:active {
transform: scale(0.95);
}
/* Modal personnalisé pour remplacer les alert */
.custom-alert-modal .modal-content {
border-radius: 12px;
border: none;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
.custom-alert-modal .modal-header {
border-bottom: none;
padding: 1.5rem 1.5rem 0.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px 12px 0 0;
}
.custom-alert-modal .modal-header .btn-close {
filter: brightness(0) invert(1);
opacity: 0.8;
}
.custom-alert-modal .modal-header .btn-close:hover {
opacity: 1;
}
.custom-alert-modal .modal-body {
padding: 1.5rem;
text-align: center;
}
.custom-alert-modal .modal-body .alert-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.custom-alert-modal .modal-body .alert-icon.success {
color: #28a745;
}
.custom-alert-modal .modal-body .alert-icon.error {
color: #dc3545;
}
.custom-alert-modal .modal-body .alert-icon.warning {
color: #ffc107;
}
.custom-alert-modal .modal-body .alert-icon.info {
color: #17a2b8;
}
.custom-alert-modal .modal-body .alert-message {
font-size: 1.1rem;
color: #333;
margin-bottom: 1rem;
line-height: 1.6;
}
.custom-alert-modal .modal-footer {
border-top: none;
padding: 0.5rem 1.5rem 1.5rem;
justify-content: center;
}
.custom-alert-modal .modal-footer .btn {
min-width: 120px;
padding: 0.6rem 1.5rem;
border-radius: 8px;
font-weight: 500;
}
/* Responsive pour single-product */
@media(max-width: 991.98px) {
.product-gallery-modern {
flex-direction: column;
}
.thumbnails-container {
flex-direction: row;
width: 100%;
overflow-x: auto;
padding: 10px 0;
}
.thumbnail-item {
flex-shrink: 0;
}
.main-image-container {
width: 100%;
}
}
@media(max-width: 768px) {
.product-gallery-modern {
flex-direction: column;
}
.thumbnails-container {
width: 100%;
flex-direction: row;
align-items: center;
}
.product-thumbnails {
flex-direction: row;
width: 100%;
max-height: 100px;
overflow-x: auto;
overflow-y: hidden;
}
.thumbnail-item {
flex-shrink: 0;
}
.thumbnail-nav-btn {
margin: 0 5px;
}
.thumbnail-nav-up {
order: 1;
}
.product-thumbnails {
order: 2;
}
.thumbnail-nav-down {
order: 3;
}
.image-overlay-icons {
opacity: 1;
}
}
</style>
{% endblock %}
{% block body %}
<!-- Start Banner Area -->
<section class="banner-area organic-breadcrumb">
<div class="container">
<div class="breadcrumb-banner d-flex flex-wrap align-items-center justify-content-end">
<div class="col-first">
<h1>Product Details Page</h1>
<nav class="d-flex align-items-center">
<a href="index.html">Home<span class="lnr lnr-arrow-right"></span>
</a>
<a href="#">Shop<span class="lnr lnr-arrow-right"></span>
</a>
<a href="single-product.html">product-details</a>
</nav>
</div>
</div>
</div>
</section>
<!-- End Banner Area -->
<!--================Single Product Area =================-->
<div class="product_image_area">
<div class="container">
<div class="row s_product_inner">
<div class="col-lg-6">
<div
class="product-gallery-modern">
<!-- Colonne de miniatures à gauche avec navigation -->
<div class="thumbnails-container">
<button class="thumbnail-nav-btn thumbnail-nav-up" id="thumbnail-nav-up" title="Image précédente">
<i class="lnr lnr-chevron-up"></i>
</button>
<div class="product-thumbnails" id="product-thumbnails">
<div class="thumbnails-wrapper" id="thumbnails-wrapper">
{% if product.images is defined and product.images|length > 0 %}
{% for img in product.images %}
<div class="thumbnail-item {% if loop.first %}active permanently-active{% endif %}" data-image="{{ asset(img) }}" data-index="{{ loop.index0 }}">
<img src="{{ asset(img) }}" alt="{{ product.name }} - Image {{ loop.index }}">
</div>
{% endfor %}
{% else %}
<div class="thumbnail-item active permanently-active" data-image="{{ asset('ui/img/category/s-p1.jpg') }}" data-index="0">
<img src="{{ asset('ui/img/category/s-p1.jpg') }}" alt="{{ product.name }}">
</div>
{% endif %}
</div>
</div>
<button class="thumbnail-nav-btn thumbnail-nav-down" id="thumbnail-nav-down" title="Image suivante">
<i class="lnr lnr-chevron-down"></i>
</button>
</div>
<!-- Grande image principale -->
<div class="product-main-image-wrapper">
<div class="product-main-image">
{% if product.images is defined and product.images|length > 0 %}
<img id="main-product-image" src="{{ asset(product.images[0]) }}" alt="{{ product.name }}">
{% else %}
<img id="main-product-image" src="{{ asset('ui/img/category/s-p1.jpg') }}" alt="{{ product.name }}">
{% endif %}
</div>
<!-- Overlay avec icônes -->
<div class="image-overlay-icons">
<button class="icon-btn zoom-btn" onclick="openImageZoom()" title="Agrandir l'image">
<i class="lnr lnr-magnifier"></i>
</button>
<button class="icon-btn favorite-btn" {% if app.user %} onclick="toggleWishlist({{ product.id }}, this); return false;" {% else %} onclick="showCustomAlert('Vous devez être connecté pour ajouter aux favoris', 'warning'); return false;" {% endif %} title="Ajouter aux favoris" data-product-id="{{ product.id }}">
<i class="lnr lnr-heart"></i>
</button>
</div>
<!-- Navigation sur l'image principale -->
<div class="main-image-nav">
<button type="button" class="main-image-nav-btn" id="main-image-prev" title="Image précédente">
<i class="lnr lnr-chevron-left"></i>
</button>
<button type="button" class="main-image-nav-btn" id="main-image-next" title="Image suivante">
<i class="lnr lnr-chevron-right"></i>
</button>
</div>
<!-- Indicateur d'image active -->
<div class="image-counter">
<span id="current-image-index">1</span>
/
<span id="total-images">{{ (product.images|length) ?: 1 }}</span>
</div>
</div>
</div>
</div>
<div class="col-lg-5 offset-lg-1">
<div class="s_product_text">
<h3>{{ product.name }}</h3>
<h2>
<span id="main-unit-price">{{ product.price|number_format(2, '.', ' ') }}</span>
HTG
</h2>
<ul class="list">
<li>
<a class="active" href="#">
<span>Category</span>
:
{{ product.category.name }}</a>
</li>
<li>
<a href="#">
<span>Disponibilité</span>
:
{{ product.stockStatus }}</a>
</li>
</ul>
<p>{{ product.description|raw|striptags|nl2br }}</p>
<!-- Informations sur la boutique -->
<div class="shop-info mb-3">
<small class="text-muted">
<i class="lnr lnr-store"></i>
Vendu par :
{% if product.shop is defined and product.shop %}
<a href="{{ path('ui_shop_show', {'slug': product.shop.slug}) }}" class="shop-link">
{{ product.shop.name }}
</a>
{% else %}
<span class="text-muted">Boutique inconnue</span>
{% endif %}
</small>
</div>
<div class="product_count">
<label for="qty">Quantité :</label>
<input type="text" name="qty" id="sst" maxlength="12" value="1" title="Quantité:" class="input-text qty" max="{{ product.stock ?? 99 }}">
<button onclick="var result = document.getElementById('sst'); var sst = result.value; if( !isNaN( sst ) && sst < {{ product.stock ?? 99 }}) result.value++;return false;" class="increase items-count" type="button">
<i class="lnr lnr-chevron-up"></i>
</button>
<button onclick="var result = document.getElementById('sst'); var sst = result.value; if( !isNaN( sst ) && sst > 1 ) result.value--;return false;" class="reduced items-count" type="button">
<i class="lnr lnr-chevron-down"></i>
</button>
</div>
<input
type="hidden" id="unit-price-input" value="{{ product.price|number_format(2, '.', '') }}"/>
{# Prix de gros (bulk pricing) #}
{% set tierPrices = [] %}
{% if product.tierPrices is defined and product.tierPrices|length > 0 %}
{% for tier in product.tierPrices %}
{% set tierPrices = tierPrices|merge([{ 'min': tier.minQty, 'price': tier.price }]) %}
{% endfor %}
{% else %}
{# Facultatif: exemple si pas de data backend #}
{% set tierPrices = [
{ 'min': 1, 'price': product.price },
{ 'min': 5, 'price': (product.price * 0.98)|round(2, 'floor') },
{ 'min': 10, 'price': (product.price * 0.95)|round(2, 'floor') },
{ 'min': 20, 'price': (product.price * 0.92)|round(2, 'floor') }
] %}
{% endif %}
<div id="bulk-pricing" class="mt-3" data-tiers='{{ tierPrices|json_encode|e('html_attr') }}'>
<div class="d-flex align-items-center mb-2">
<strong class="me-2">Prix unitaire selon quantité :</strong>
<span id="unit-price-value">{{ product.price|number_format(2, '.', ' ') }}
HTG</span>
</div>
<div class="table-responsive">
<table class="table table-sm mb-2" style="border:1px solid #eee;">
<thead class="table-light">
<tr>
<th>Quantité min.</th>
<th>Prix unitaire (HTG)</th>
</tr>
</thead>
<tbody id="bulk-tier-rows">
{% for tier in tierPrices|sort((a,b)=> a.min <=> b.min) %}
<tr data-min="{{ tier.min }}">
<td>≥
{{ tier.min }}</td>
<td>{{ tier.price|number_format(2, '.', ' ') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="d-flex flex-wrap gap-3 align-items-baseline mb-2">
<div>
<strong>Total:</strong>
<span id="total-price-value">{{ product.price|number_format(2, '.', ' ') }}</span>
HTG
</div>
<div class="text-success">
<strong>Économies:</strong>
<span id="savings-amount">0.00</span>
HTG
(<span id="savings-percent">0</span>%)
</div>
</div>
<small class="text-muted">Le prix et le total s’adaptent automatiquement selon la quantité.</small>
</div>
<!-- Stock disponible -->
{% if product.stock is defined %}
<small class="text-muted">Stock disponible :
{{ product.stock }}
unités</small>
{% endif %}
<!-- Statistiques du produit -->
{% if productStats is defined %}
<div class="product-stats mt-3">
<div class="row text-center">
<div class="col-4">
<div class="stat-item">
<span class="stat-number">{{ productStats.totalViews }}</span>
<small class="stat-label">Vues</small>
</div>
</div>
<div class="col-4">
<div class="stat-item">
<span class="stat-number">{{ productStats.salesCount }}</span>
<small class="stat-label">Ventes</small>
</div>
</div>
<div class="col-4">
<div class="stat-item">
<span class="stat-number">{{ productStats.conversionRate }}%</span>
<small class="stat-label">Conversion</small>
</div>
</div>
</div>
</div>
{% endif %}
<div class="card_area d-flex align-items-center">
<a class="primary-btn btn-add-to-cart" href="javascript:void(0);" id="add-to-cart-btn" data-product-id="{{ product.id }}" data-qty="1">
<i class="lnr lnr-cart"></i>
Ajouter au panier
</a>
<a class="icon_btn wishlist-btn" href="#" title="Ajouter aux favoris" data-product-id="{{ product.id }}" {% if app.user %} onclick="toggleWishlist({{ product.id }}, this); return false;" {% else %} onclick="showCustomAlert('Vous devez être connecté pour ajouter aux favoris', 'warning'); return false;" {% endif %}>
<i class="lnr lnr-heart"></i>
</a>
<a class="icon_btn comparison-btn" href="#" title="Comparer" data-product-id="{{ product.id }}" onclick="toggleComparison({{ product.id }}, this); return false;">
<i class="lnr lnr-sync"></i>
</a>
</div>
<!-- Message de confirmation -->
<div id="cart-message" class="alert alert-success mt-3" style="display: none;">
<i class="lnr lnr-checkmark-circle"></i>
Produit ajouté au panier avec succès !
</div>
<!-- ... existing code ... -->
</div>
</div>
</div>
</div>
</div>
<!--================End Single Product Area =================-->
<!--================Product Description Area =================-->
<section class="product_description_area">
<div class="container">
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="home-tab" data-bs-toggle="tab" href="#home" role="tab" aria-controls="home" aria-selected="true">Description</a>
</li>
<li class="nav-item">
<a class="nav-link" id="profile-tab" data-bs-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="false">Spécifications</a>
</li>
<li class="nav-item">
<a class="nav-link" id="contact-tab" data-bs-toggle="tab" href="#contact" role="tab" aria-controls="contact" aria-selected="false">Commentaires</a>
</li>
<li class="nav-item">
<a class="nav-link" id="review-tab" data-bs-toggle="tab" href="#review" role="tab" aria-controls="review" aria-selected="false">Avis ({{ product.reviewCount ?? 0 }})</a>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
<div class="description">
{% if product.description %}
<div class="product-description-content">
{{ product.description|raw|nl2br }}
</div>
{% else %}
<p class="text-muted">Aucune description disponible pour ce produit.</p>
{% endif %}
</div>
</div>
<div class="tab-pane fade" id="profile" role="tabpanel" aria-labelledby="profile-tab">
<div class="specification-table">
{% if product.weight or product.length or product.width or product.height or product.attributes %}
<div class="table-responsive">
<table class="table">
<tbody>
{% if product.sku %}
<tr>
<td>
<h5>Référence (SKU)</h5>
</td>
<td>
<h5>{{ product.sku }}</h5>
</td>
</tr>
{% endif %}
{% if product.barcode %}
<tr>
<td>
<h5>Code-barres</h5>
</td>
<td>
<h5>{{ product.barcode }}</h5>
</td>
</tr>
{% endif %}
{% if product.brand %}
<tr>
<td>
<h5>Marque</h5>
</td>
<td>
<h5>{{ product.brand.name }}</h5>
</td>
</tr>
{% endif %}
{% if product.category %}
<tr>
<td>
<h5>Catégorie</h5>
</td>
<td>
<h5>{{ product.category.name }}</h5>
</td>
</tr>
{% endif %}
{% if product.weight %}
<tr>
<td>
<h5>Poids</h5>
</td>
<td>
<h5>{{ product.weight }}
kg</h5>
</td>
</tr>
{% endif %}
{% if product.length or product.width or product.height %}
<tr>
<td>
<h5>Dimensions</h5>
</td>
<td>
<h5>
{% if product.length %}
{{ product.length }}cm
{% endif %}
{% if product.width %}
×
{{ product.width }}cm
{% endif %}
{% if product.height %}
×
{{ product.height }}cm
{% endif %}
</h5>
</td>
</tr>
{% endif %}
{% if product.stock is not null %}
<tr>
<td>
<h5>Stock disponible</h5>
</td>
<td>
<h5>{{ product.stock }}
unité(s)</h5>
</td>
</tr>
{% endif %}
{% if product.stockStatus %}
<tr>
<td>
<h5>Statut</h5>
</td>
<td>
<h5>
{% if product.stockStatus == 'in_stock' %}
<span class="badge bg-success">En stock</span>
{% elseif product.stockStatus == 'out_of_stock' %}
<span class="badge bg-danger">Rupture de stock</span>
{% elseif product.stockStatus == 'backorder' %}
<span class="badge bg-warning">Sur commande</span>
{% else %}
{{ product.stockStatus }}
{% endif %}
</h5>
</td>
</tr>
{% endif %}
{% if product.isDigital %}
<tr>
<td>
<h5>Type</h5>
</td>
<td>
<h5>
<span class="badge bg-info">Produit digital</span>
</h5>
</td>
</tr>
{% endif %}
{% if product.attributes is not empty %}
{% for key, value in product.attributes %}
<tr>
<td>
<h5>{{ key|replace({'_': ' '})|title }}</h5>
</td>
<td>
<h5>{{ value }}</h5>
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">Aucune spécification disponible pour ce produit.</p>
{% endif %}
</div>
</div>
<div class="tab-pane fade" id="contact" role="tabpanel" aria-labelledby="contact-tab">
<div class="comment-wrapper">
<div class="alert alert-info">
<i class="lnr lnr-info-circle"></i>
La fonctionnalité de commentaires sera bientôt disponible. Vous pourrez laisser des commentaires et poser des questions sur ce produit.
</div>
{% if app.user %}
<div class="review_box mt-4">
<h4>Poser une question</h4>
<form class="row contact_form" method="post" novalidate="novalidate">
<div class="col-md-12">
<div class="form-group">
<textarea class="form-control" name="comment" id="comment" rows="3" placeholder="Votre question ou commentaire..." required></textarea>
</div>
</div>
<div class="col-md-12 text-right">
<button type="submit" class="btn primary-btn">Envoyer</button>
</div>
</form>
</div>
{% else %}
<p class="text-center">
<a href="{{ path('ui_app_login') }}" class="primary-btn">Connectez-vous</a>
pour poser une question ou laisser un commentaire.
</p>
{% endif %}
</div>
</div>
<div class="tab-pane fade" id="review" role="tabpanel" aria-labelledby="review-tab">
<div class="review-wrapper">
<div class="row">
<div class="col-lg-6">
<div class="row total_rate">
<div class="col-6">
<div class="box_total">
<h5>Note globale</h5>
<h4>{{ product.averageRating ? product.averageRating|number_format(1, '.', ',') : '0.0' }}</h4>
<h6>({{ product.reviewCount ?? 0 }}
Avis)</h6>
</div>
</div>
<div class="col-6">
<div class="rating_list">
<h3>Basé sur
{{ product.reviewCount ?? 0 }}
avis</h3>
<div class="rating-summary">
{% if product.averageRating %}
<div class="rating-display mb-3">
{% for i in 1..5 %}
<i class="fa fa-star{% if i <= product.averageRating %}{% else %}-o{% endif %}" style="color: #ffa200; font-size: 1.2rem;"></i>
{% endfor %}
<span class="ms-2" style="font-size: 1.1rem; font-weight: bold;">{{ product.averageRating|number_format(1, '.', ',') }}
/ 5.0</span>
</div>
{% else %}
<p class="text-muted">Aucune note disponible</p>
{% endif %}
</div>
</div>
</div>
</div>
<div class="review_list">
{% if product.reviewCount and product.reviewCount > 0 %}
<div class="alert alert-info">
<i class="lnr lnr-info-circle"></i>
Le système d'affichage détaillé des avis sera disponible prochainement.
Note actuelle :
{{ product.averageRating|number_format(1, '.', ',') }}
/ 5.0 basée sur
{{ product.reviewCount }}
avis.
</div>
{% else %}
<div class="alert alert-info">
<i class="lnr lnr-info-circle"></i>
Aucun avis pour ce produit pour le moment. Soyez le premier à laisser un avis après votre achat !
</div>
{% endif %}
</div>
</div>
<div class="col-lg-6">
<div class="review_box">
<h4>Laisser un avis</h4>
{% if app.user %}
<p class="text-muted">Vous pouvez laisser un avis après avoir acheté ce produit.</p>
<form class="row contact_form" method="post" novalidate="novalidate">
<div class="col-md-12">
<div class="form-group">
<label>Votre note</label>
<div class="rating-input mb-3" style="display: flex; gap: 5px; flex-direction: row-reverse; justify-content: flex-end;">
{% for i in 5..1 %}
<input type="radio" name="rating" id="rating{{ i }}" value="{{ i }}" required style="display: none;">
<label for="rating{{ i }}" class="star-label" style="cursor: pointer;">
<i class="fa fa-star-o" style="color: #ccc; font-size: 1.5rem;"></i>
</label>
{% endfor %}
</div>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<textarea class="form-control" name="review" id="review" rows="5" placeholder="Votre avis..." required onfocus="this.placeholder = ''" onblur="this.placeholder = 'Votre avis...'"></textarea>
</div>
</div>
<div class="col-md-12 text-right">
<button type="submit" class="btn primary-btn">Publier l'avis</button>
</div>
</form>
{% else %}
<p class="text-center">
<a href="{{ path('ui_app_login') }}" class="primary-btn">Connectez-vous</a>
pour laisser un avis sur ce produit.
</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!--================End Product Description Area =================-->
<script>
// Initialiser les onglets Bootstrap
document.addEventListener('DOMContentLoaded', function () { // Activer les onglets Bootstrap 5
const triggerTabList = document.querySelectorAll('#myTab button[data-bs-toggle="tab"]');
triggerTabList.forEach(triggerEl => {
const tabTrigger = new bootstrap.Tab(triggerEl);
triggerEl.addEventListener('click', event => {
event.preventDefault();
tabTrigger.show();
});
});
// Gestion des étoiles pour les avis
const ratingInputs = document.querySelectorAll('.rating-input input[type="radio"]');
const starLabels = document.querySelectorAll('.rating-input .star-label');
ratingInputs.forEach((input, index) => {
input.addEventListener('change', function () {
const rating = parseInt(this.value);
starLabels.forEach((label) => { // Trouver l'input associé à ce label
const labelInput = label.previousElementSibling;
if (labelInput && labelInput.type === 'radio') {
const starValue = parseInt(labelInput.value);
const star = label.querySelector('i');
if (starValue <= rating) {
star.style.color = '#ffa200';
star.classList.remove('fa-star-o');
star.classList.add('fa-star');
} else {
star.style.color = '#ccc';
star.classList.remove('fa-star');
star.classList.add('fa-star-o');
}
}
});
});
});
// Pré-remplir les étoiles au survol
starLabels.forEach((label) => {
label.addEventListener('mouseenter', function () { // Trouver l'input associé à ce label
const input = label.previousElementSibling;
if (input && input.type === 'radio') {
const rating = parseInt(input.value);
starLabels.forEach((l) => {
const lInput = l.previousElementSibling;
if (lInput && lInput.type === 'radio') {
const starValue = parseInt(lInput.value);
const star = l.querySelector('i');
if (starValue <= rating) {
star.style.color = '#ffa200';
star.classList.remove('fa-star-o');
star.classList.add('fa-star');
} else {
star.style.color = '#ccc';
star.classList.remove('fa-star');
star.classList.add('fa-star-o');
}
}
});
}
});
});
const ratingContainer = document.querySelector('.rating-input');
if (ratingContainer) {
ratingContainer.addEventListener('mouseleave', function () {
const checkedInput = document.querySelector('.rating-input input[type="radio"]:checked');
if (checkedInput) {
checkedInput.dispatchEvent(new Event('change'));
} else {
starLabels.forEach(label => {
const star = label.querySelector('i');
star.style.color = '#ccc';
});
}
});
}
});
</script>
{% if youMightAlsoLike|length > 0 %}
<section class="related-product-area section_gap_bottom py-5" style="background: linear-gradient(to bottom, #f8f9fa 0%, #ffffff 100%);">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8 text-center">
<div class="section-title">
<h2 class="fw-bold mb-3" style="font-size: 2.5rem; color: #2c3e50;">Vous pourriez aussi aimer</h2>
<p class="text-muted" style="font-size: 1.1rem;">Produits sélectionnés pour vous en fonction de vos préférences</p>
</div>
</div>
</div>
<div class="row g-4">
{% for relatedProduct in youMightAlsoLike %}
<div class="col-xl-3 col-lg-4 col-md-6 col-sm-6 mb-4 d-flex align-items-stretch">
{% include 'home/_single_product_card.html.twig' with {'product': relatedProduct} %}
</div>
{% endfor %}
</div>
</div>
</section>
<style>
.modern-product-card {
background: #ffffff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
height: 100%;
display: flex;
flex-direction: column;
position: relative;
}
.modern-product-card:hover {
transform: translateY(-8px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
}
.product-image-wrapper {
position: relative;
width: 100%;
height: 250px;
overflow: hidden;
background: #f8f9fa;
}
.product-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.modern-product-card:hover .product-image {
transform: scale(1.1);
}
.product-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.modern-product-card:hover .product-overlay {
opacity: 1;
}
.view-product-btn {
color: white;
padding: 12px 24px;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border-radius: 8px;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.view-product-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
}
.product-badge {
position: absolute;
top: 12px;
right: 12px;
z-index: 2;
}
.badge-discount {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: white;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 700;
box-shadow: 0 2px 8px rgba(238, 90, 111, 0.4);
}
.product-info {
padding: 20px;
flex-grow: 1;
display: flex;
flex-direction: column;
}
.product-title {
margin: 0 0 8px 0;
font-size: 1rem;
font-weight: 600;
line-height: 1.4;
height: 2.8em;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.product-title a {
color: #2c3e50;
text-decoration: none;
transition: color 0.3s ease;
}
.product-title a:hover {
color: #ffa200;
}
.product-shop {
font-size: 0.85rem;
color: #6c757d;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 6px;
}
.product-shop i {
font-size: 0.9rem;
}
.shop-link {
color: #6c757d;
text-decoration: none;
transition: color 0.3s ease;
}
.shop-link:hover {
color: #ffa200;
}
.product-price-wrapper {
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.product-price {
font-size: 1.25rem;
font-weight: 700;
color: #ffa200;
}
.product-compare-price {
font-size: 0.9rem;
color: #adb5bd;
text-decoration: line-through;
}
.product-actions {
margin-top: auto;
}
/* Styles pour les boutons de la section "Vous pourriez aussi aimer" uniquement */
.modern-product-card .btn-add-to-cart {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #ffa200 0%, #e8910a 100%);
color: white;
border: none;
border-radius: 10px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
text-decoration: none;
transition: all 0.3s ease;
cursor: pointer;
}
.modern-product-card .btn-add-to-cart:hover {
background: linear-gradient(135deg, #e8910a 0%, #d6820a 100%);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 162, 0, 0.4);
color: white;
}
/* Le bouton principal garde son design original */
.card_area .btn-add-to-cart {
width: auto;
display: inline-flex;
}
.modern-product-card .btn-add-to-cart:active {
transform: translateY(0);
}
.modern-product-card .btn-add-to-cart i {
font-size: 1.1rem;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.product-image-wrapper {
height: 200px;
}
.product-info {
padding: 16px;
}
.product-title {
font-size: 0.95rem;
}
.product-price {
font-size: 1.1rem;
}
.section-title h2 {
font-size: 2rem !important;
}
}
@media (max-width: 576px) {
.product-image-wrapper {
height: 190px;
}
.product-info {
padding: 14px;
}
.btn-add-to-cart {
padding: 10px;
font-size: 0.9rem;
}
}
/* Ensure fixed height for cards */
.col-6.col-md-4.col-lg-3 {
display: flex;
}
.modern-product-card {
width: 100%;
}
/* Loading state for add to cart button */
.btn-add-to-cart.loading {
opacity: 0.7;
pointer-events: none;
}
.btn-add-to-cart.loading i {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Success state - vert pour tous les boutons */
.btn-add-to-cart.success {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%) !important;
}
/* Le bouton principal garde son style original même en success */
.card_area .btn-add-to-cart.success {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%) !important;
}
</style>
<script>
// Gestion des boutons d'ajout au panier (principal et section "Vous pourriez aussi aimer")
document.addEventListener('DOMContentLoaded', function() {
const addToCartButtons = document.querySelectorAll('.btn-add-to-cart');
addToCartButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const productId = this.getAttribute('data-product-id');
// Pour le bouton principal, récupérer la quantité depuis l'input sst si disponible
let qty = this.getAttribute('data-qty') || '1';
if (this.id === 'add-to-cart-btn') {
const qtyInput = document.getElementById('sst');
if (qtyInput) {
qty = parseInt(qtyInput.value) || 1;
}
}
if (!productId) {
console.error('Product ID manquant');
return false;
}
// Sauvegarder le contenu original
const originalContent = this.innerHTML;
// Désactiver le bouton et afficher le loading
this.classList.add('loading');
if (this.id === 'add-to-cart-btn') {
this.style.pointerEvents = 'none';
this.innerHTML = '<i class="lnr lnr-spinner"></i> Ajout en cours...';
} else {
this.innerHTML = '<i class="ti ti-loader-2"></i> <span>Ajout...</span>';
}
// Construire le body de la requête
let body = 'productId=' + encodeURIComponent(productId) + '&qty=' + encodeURIComponent(qty);
// Pour le bouton principal, vérifier aussi le prix unitaire pour les prix de gros
if (this.id === 'add-to-cart-btn') {
const unitPriceInput = document.getElementById('unit-price-input');
const unitPrice = unitPriceInput ? unitPriceInput.value : '';
if (unitPrice) {
body += '&unitPrice=' + encodeURIComponent(unitPrice);
}
}
// Faire la requête AJAX
fetch('{{ path("ui_cart_add") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: body
})
.then(response => {
if (!response.ok) {
throw new Error('Erreur réseau: ' + response.status);
}
return response.json();
})
.then(data => {
if (data.ok || data.success) {
// Succès - Afficher l'animation de succès
this.classList.remove('loading');
this.classList.add('success');
if (this.id === 'add-to-cart-btn') {
this.style.background = 'linear-gradient(135deg, #28a745 0%, #20c997 100%)';
this.innerHTML = '<i class="lnr lnr-checkmark-circle"></i> Ajouté !';
// Afficher le message de confirmation si présent
const cartMessage = document.getElementById('cart-message');
if (cartMessage) {
cartMessage.style.display = 'block';
setTimeout(() => {
cartMessage.style.display = 'none';
}, 3000);
}
} else {
this.innerHTML = '<i class="ti ti-check"></i> <span>Ajouté !</span>';
}
// Mettre à jour le compteur du panier si présent
const cartBadge = document.querySelector('.cart-badge');
if (cartBadge) {
cartBadge.textContent = data.totalQty || data.quantity || 0;
// Animation du badge
cartBadge.style.transform = 'scale(1.2)';
setTimeout(() => {
cartBadge.style.transform = 'scale(1)';
}, 300);
}
// Afficher une notification si la fonction existe
if (typeof showCustomAlert === 'function') {
showCustomAlert('Produit ajouté au panier avec succès !', 'success');
}
// Restaurer le bouton après 2 secondes
setTimeout(() => {
this.classList.remove('success');
if (this.id === 'add-to-cart-btn') {
this.style.background = '';
this.style.pointerEvents = 'auto';
}
this.innerHTML = originalContent;
}, 2000);
} else {
// Erreur
this.classList.remove('loading');
if (this.id === 'add-to-cart-btn') {
this.style.pointerEvents = 'auto';
}
this.innerHTML = originalContent;
const message = data.message || 'Erreur lors de l\'ajout au panier';
if (typeof showCustomAlert === 'function') {
showCustomAlert(message, 'error');
} else {
alert(message);
}
}
})
.catch(error => {
console.error('Erreur:', error);
this.classList.remove('loading');
if (this.id === 'add-to-cart-btn') {
this.style.pointerEvents = 'auto';
}
this.innerHTML = originalContent;
if (typeof showCustomAlert === 'function') {
showCustomAlert('Erreur lors de l\'ajout au panier. Veuillez réessayer.', 'error');
} else {
alert('Erreur lors de l\'ajout au panier. Veuillez réessayer.');
}
});
return false;
});
});
});
</script>
{% endif %}
<script src="js/vendor/jquery-2.2.4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous"></script>
<script src="js/vendor/bootstrap.min.js"></script>
<script src="js/jquery.ajaxchimp.min.js"></script>
<script src="js/jquery.nice-select.min.js"></script>
<script src="js/jquery.sticky.js"></script>
<script src="js/nouislider.min.js"></script>
<script src="js/jquery.magnific-popup.min.js"></script>
<script src="js/owl.carousel.min.js"></script>
<!--gmaps Js-->
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCjCGmQ0Uq4exrzdcL6rvxywDDOvfAu6eE"></script>
<script src="js/gmaps.min.js"></script>
<script src="js/main.js"></script>
<script>
// Le gestionnaire d'événements pour les boutons d'ajout au panier est déjà défini
// dans la section "Vous pourriez aussi aimer" et gère maintenant aussi le bouton principal
// Gestion de la galerie d'images
document.addEventListener('DOMContentLoaded', function () {
const thumbnails = document.querySelectorAll('.thumbnail-item');
const mainImage = document.getElementById('main-product-image');
const currentIndex = document.getElementById('current-image-index');
const totalImages = document.getElementById('total-images');
// Fonction pour changer l'image principale
function changeMainImage(thumbnail, index) {
const imageUrl = thumbnail.getAttribute('data-image');
if (mainImage && imageUrl) {
mainImage.style.opacity = '0';
setTimeout(() => {
mainImage.src = imageUrl;
mainImage.style.opacity = '1';
// Reset zoom
mainImage.style.transform = 'scale(1)';
mainImage.style.transformOrigin = 'center center';
}, 150);
}
// Mettre à jour l'indicateur
if (currentIndex) {
currentIndex.textContent = index + 1;
}
}
// Ajouter le zoom sur l'image principale au survol
const mainImageContainer = document.querySelector('.product-main-image-wrapper');
if (mainImageContainer && mainImage) {
mainImageContainer.addEventListener('mousemove', function(e) {
const rect = mainImageContainer.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const xPercent = (x / rect.width) * 100;
const yPercent = (y / rect.height) * 100;
mainImage.style.transformOrigin = `${xPercent}% ${yPercent}%`;
mainImage.style.transform = 'scale(2.2)'; // Zoom Alibaba-style
});
mainImageContainer.addEventListener('mouseleave', function() {
mainImage.style.transformOrigin = 'center center';
mainImage.style.transform = 'scale(1)';
});
}
// Gérer le survol sur les miniatures (sélection permanente au survol)
thumbnails.forEach((thumbnail, index) => {
thumbnail.addEventListener('mouseenter', function () { // Retirer toutes les sélections permanentes
thumbnails.forEach(t => {
t.classList.remove('active', 'permanently-active');
});
// Ajouter active et permanently-active à la miniature survolée
thumbnail.classList.add('active', 'permanently-active');
// Changer l'image principale
changeMainImage(thumbnail, index);
});
});
// Gérer le clic sur les miniatures (sélection permanente)
thumbnails.forEach((thumbnail, index) => {
thumbnail.addEventListener('click', function () { // Retirer la classe active et permanently-active de toutes les miniatures
thumbnails.forEach(t => {
t.classList.remove('active', 'permanently-active');
});
// Ajouter les classes active et permanently-active à la miniature cliquée
thumbnail.classList.add('active', 'permanently-active');
// Changer l'image principale
changeMainImage(thumbnail, index);
});
});
// Gestion du carrousel de miniatures avec taille fixe carrée
const navUpBtn = document.getElementById('thumbnail-nav-up');
const navDownBtn = document.getElementById('thumbnail-nav-down');
const thumbnailsContainer = document.getElementById('product-thumbnails');
const thumbnailsWrapper = document.getElementById('thumbnails-wrapper');
const thumbnailItems = Array.from(document.querySelectorAll('.thumbnail-item'));
let currentThumbnailOffset = 0;
const thumbnailHeight = 90;
// 80px (hauteur) + 10px (gap)
// Calculer la hauteur disponible pour les miniatures dynamiquement
const thumbnailsEl = document.getElementById('product-thumbnails');
let containerHeight = thumbnailsEl ? thumbnailsEl.clientHeight : 520; // Hauteur par défaut si non disponible
let visibleThumbnails = Math.floor(containerHeight / thumbnailHeight);
// Recalculer après le chargement pour avoir la vraie hauteur
setTimeout(() => {
if (thumbnailsEl) {
containerHeight = thumbnailsEl.clientHeight;
visibleThumbnails = Math.floor(containerHeight / thumbnailHeight);
updateThumbnailsPosition();
}
}, 100);
function getCurrentIndex() {
const current = document.querySelector('.thumbnail-item.permanently-active') || document.querySelector('.thumbnail-item.active');
if (! current)
return 0;
const idxAttr = current.getAttribute('data-index');
return idxAttr ? parseInt(idxAttr, 10) : Math.max(0, thumbnailItems.indexOf(current));
}
function selectThumbnailByIndex(newIndex) {
if (! thumbnailItems.length)
return;
const total = thumbnailItems.length;
if (newIndex < 0)
newIndex = 0;
if (newIndex > total - 1)
newIndex = total - 1;
// Reset classes
thumbnailItems.forEach(t => t.classList.remove('active', 'permanently-active'));
const target = thumbnailItems[newIndex];
if (! target)
return;
target.classList.add('active', 'permanently-active');
changeMainImage(target, newIndex);
// Ajuster le carrousel pour voir la miniature sélectionnée
ensureThumbnailVisible(newIndex);
}
function ensureThumbnailVisible(index) {
if (! thumbnailsWrapper || ! thumbnailItems.length)
return;
const targetTop = index * thumbnailHeight;
const visibleTop = currentThumbnailOffset * thumbnailHeight;
const visibleBottom = visibleTop + containerHeight;
// Si la miniature est au-dessus de la zone visible
if (targetTop < visibleTop) {
currentThumbnailOffset = index;
updateThumbnailsPosition();
}
// Si la miniature est en-dessous de la zone visible
else if (targetTop + thumbnailHeight > visibleBottom) {
currentThumbnailOffset = Math.max(0, index - visibleThumbnails + 1);
updateThumbnailsPosition();
}
}
function updateThumbnailsPosition() {
if (!thumbnailsWrapper)
return;
const maxOffset = Math.max(0, thumbnailItems.length - visibleThumbnails);
currentThumbnailOffset = Math.max(0, Math.min(currentThumbnailOffset, maxOffset));
thumbnailsWrapper.style.transform = `translateY(-${
currentThumbnailOffset * thumbnailHeight
}px)`;
updateNavButtons();
}
function updateNavButtons() {
if (!thumbnailItems.length)
return;
const maxOffset = Math.max(0, thumbnailItems.length - visibleThumbnails);
const canScrollUp = currentThumbnailOffset > 0;
const canScrollDown = currentThumbnailOffset < maxOffset;
if (navUpBtn) {
navUpBtn.classList.toggle('disabled', ! canScrollUp);
}
if (navDownBtn) {
navDownBtn.classList.toggle('disabled', ! canScrollDown);
}
}
// Navigation avec les boutons
if(navUpBtn) {
navUpBtn.addEventListener('click', function (e) {
e.preventDefault();
if (currentThumbnailOffset > 0) {
currentThumbnailOffset --;
updateThumbnailsPosition();
}
});
}
if(navDownBtn) {
navDownBtn.addEventListener('click', function (e) {
e.preventDefault();
const maxOffset = Math.max(0, thumbnailItems.length - visibleThumbnails);
if (currentThumbnailOffset < maxOffset) {
currentThumbnailOffset ++;
updateThumbnailsPosition();
}
});
}
// Initialiser le carrousel
if(thumbnailsWrapper && thumbnailItems.length > 0) {
updateThumbnailsPosition();
// S'assurer que la première miniature est visible
ensureThumbnailVisible(0);
}
// Navigation sur l'image principale
const mainPrev = document.getElementById('main-image-prev');
const mainNext = document.getElementById('main-image-next');
if(mainPrev) {
mainPrev.addEventListener('click', function (e) {
e.preventDefault();
const idx = getCurrentIndex();
selectThumbnailByIndex(idx - 1);
});
}
if(mainNext) {
mainNext.addEventListener('click', function (e) {
e.preventDefault();
const idx = getCurrentIndex();
selectThumbnailByIndex(idx + 1);
});
}
}
);
// --- Prix de gros : calcul du prix unitaire dynamique ---
(function () {
const bulk = document.getElementById('bulk-pricing');
const qtyInputEl = document.getElementById('sst');
const unitPriceSpan = document.getElementById('unit-price-value');
const mainUnitPriceSpan = document.getElementById('main-unit-price');
const totalPriceSpan = document.getElementById('total-price-value');
const savingsAmountSpan = document.getElementById('savings-amount');
const savingsPercentSpan = document.getElementById('savings-percent');
const unitPriceInput = document.getElementById('unit-price-input');
const tierRowsTbody = document.getElementById('bulk-tier-rows');
if (! bulk || ! qtyInputEl || ! unitPriceSpan || ! mainUnitPriceSpan || ! unitPriceInput)
return;
let tiers = [];
try {
tiers = JSON.parse(bulk.getAttribute('data-tiers') || '[]');
} catch (e) {
tiers = [];
}
if (!Array.isArray(tiers) || tiers.length === 0)
return;
tiers.sort(function (a, b) {
return(a.min || 0) - (b.min || 0);
});
const basePrice = (function () {
const one = tiers.find(t => (t.min || 0) <= 1);
return one ? parseFloat(one.price) : parseFloat(tiers[0].price);
})();
function getUnitPriceForQty(qty) {
let candidate = tiers[0] || null;
for (let i = 0; i < tiers.length; i++) {
if (qty >= (tiers[i].min || 1)) {
candidate = tiers[i];
}
}
return candidate ? parseFloat(candidate.price) : (tiers[0] ? parseFloat(tiers[0].price) : basePrice);
}
function formatPrice(val) {
return(parseFloat(val) || 0).toFixed(2);
}
function highlightActiveTier(qty) {
if (! tierRowsTbody)
return;
const rows = tierRowsTbody.querySelectorAll('tr');
rows.forEach(r => r.classList.remove('table-warning'));
let activeRow = null;
rows.forEach(r => {
const m = parseInt(r.getAttribute('data-min'), 10) || 1;
if (qty >= m)
activeRow = r;
});
if (activeRow)
activeRow.classList.add('table-warning');
}
function refreshPrices() {
const qty = Math.max(1, parseInt(qtyInputEl.value, 10) || 1);
const unit = getUnitPriceForQty(qty);
const total = unit * qty;
const saveAmount = Math.max(0, (basePrice - unit) * qty);
const savePercent = Math.max(0, 100 * (basePrice - unit) / basePrice);
unitPriceSpan.textContent = formatPrice(unit) + ' HTG';
mainUnitPriceSpan.textContent = formatPrice(unit);
if (totalPriceSpan)
totalPriceSpan.textContent = formatPrice(total);
if (savingsAmountSpan)
savingsAmountSpan.textContent = formatPrice(saveAmount);
if (savingsPercentSpan)
savingsPercentSpan.textContent = Math.round(savePercent);
unitPriceInput.value = formatPrice(unit);
highlightActiveTier(qty);
}
qtyInputEl.addEventListener('input', refreshPrices);
qtyInputEl.addEventListener('change', refreshPrices);
setTimeout(refreshPrices, 50);
setInterval(refreshPrices, 350);
})();
// --- /Prix de gros ---
// Injection du prix unitaire dans l'ajout au panier
(function () {
const addToCartBtn = document.getElementById('add-to-cart-btn');
if (! addToCartBtn)
return;
addToCartBtn.addEventListener('click', function () {
const unitPriceInput = document.getElementById('unit-price-input');
if (unitPriceInput) {
addToCartBtn.setAttribute('data-unit-price', unitPriceInput.value);
}
});
})();
// Fonction pour ouvrir le modal de zoom avec loader
function openImageZoom () {
const mainImage = document.getElementById('main-product-image');
const zoomModal = document.getElementById('image-zoom-modal');
const zoomImage = document.getElementById('zoom-modal-image');
const zoomLoader = document.getElementById('zoom-loader');
if (mainImage && zoomModal && zoomImage) { // Afficher le loader
if (zoomLoader) {
zoomLoader.style.display = 'flex';
}
// Masquer l'image et réinitialiser
zoomImage.classList.remove('loaded');
zoomImage.style.opacity = '0';
// Ouvrir le modal avec animation
zoomModal.classList.add('active');
document.body.style.overflow = 'hidden';
// Charger l'image
const imageUrl = mainImage.src;
const tempImage = new Image();
tempImage.onload = function () {
zoomImage.src = imageUrl;
// Masquer le loader et afficher l'image avec transition
setTimeout(function () {
if (zoomLoader) {
zoomLoader.style.display = 'none';
}
zoomImage.classList.add('loaded');
zoomImage.style.opacity = '1';
}, 200);
};
tempImage.onerror = function () {
if (zoomLoader) {
zoomLoader.style.display = 'none';
}
zoomImage.src = imageUrl;
zoomImage.classList.add('loaded');
};
tempImage.src = imageUrl;
}
}
// Fonction pour fermer le modal de zoom avec animation
function closeImageZoom () {
const zoomModal = document.getElementById('image-zoom-modal');
const zoomImage = document.getElementById('zoom-modal-image');
const zoomLoader = document.getElementById('zoom-loader');
if (zoomModal) { // Animation de fermeture
if (zoomImage) {
zoomImage.style.opacity = '0';
zoomImage.classList.remove('loaded');
}
setTimeout(function () {
zoomModal.classList.remove('active');
document.body.style.overflow = 'auto';
// Réinitialiser le loader pour la prochaine ouverture
if (zoomLoader) {
zoomLoader.style.display = 'none';
}
}, 200);
}
}
// Fermer le modal en cliquant en dehors
document.addEventListener('DOMContentLoaded', function () {
const zoomModal = document.getElementById('image-zoom-modal');
if (zoomModal) {
zoomModal.addEventListener('click', function (e) {
if (e.target === zoomModal) {
closeImageZoom();
}
});
}
});
</script>
<!-- Modal de zoom d'image -->
<div id="image-zoom-modal" class="image-zoom-modal">
<span class="close-zoom" onclick="closeImageZoom()" title="Fermer">×</span>
<div
class="zoom-modal-content">
<!-- Loader -->
<div class="zoom-loader" id="zoom-loader" style="display: none;">
<div class="zoom-loader-spinner"></div>
<div class="zoom-loader-text">Chargement...</div>
</div>
<!-- Image -->
<img id="zoom-modal-image" src="" alt="{{ product.name }}">
</div>
</div>
{% if dropshipReferral %}
<!-- Modal d'affiliation -->
<div id="dropship-modal" class="dropship-referral-modal" style="display: none;">
<div class="dropship-modal-overlay"></div>
<div class="dropship-modal-content">
<button type="button" class="dropship-modal-close" onclick="closeDropshipModal()" aria-label="Fermer">
×
</button>
<div class="dropship-modal-header">
<div class="dropship-icon">
<i class="fas fa-gift"></i>
</div>
<h3>Produit recommandé par
{{ dropshipReferral.affiliateName }}</h3>
<p class="dropship-subtitle">Vous avez été dirigé vers ce produit par un de nos partenaires</p>
</div>
<div class="dropship-modal-body">
<div class="dropship-product-info">
{% if product.images|length > 0 %}
<img src="{{ asset(product.images[0]) }}" alt="{{ product.name }}" class="dropship-product-image" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="dropship-product-image-placeholder" style="display: none; width: 100px; height: 100px; background: #f0f0f0; border-radius: 8px; align-items: center; justify-content: center; color: #999;">
<i class="fas fa-image" style="font-size: 32px;"></i>
</div>
{% else %}
<div class="dropship-product-image-placeholder" style="width: 100px; height: 100px; background: #f0f0f0; border-radius: 8px; display: flex; align-items: center; justify-content: center; color: #999;">
<i class="fas fa-image" style="font-size: 32px;"></i>
</div>
{% endif %}
<div class="dropship-product-details">
<h4>{{ product.name }}</h4>
<div class="dropship-product-price">
<span class="price">{{ product.price|number_format(2, ',', ' ') }}
HTG</span>
</div>
</div>
</div>
<div class="dropship-message">
<p>
<strong>{{ dropshipReferral.affiliateName }}</strong>
vous recommande ce produit. Souhaitez-vous l'ajouter à votre panier ?</p>
</div>
</div>
<div class="dropship-modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeDropshipModal()">
Parcourir d'abord
</button>
<button type="button" class="btn btn-primary dropship-add-to-cart" onclick="addToCartFromDropship()">
<i class="fas fa-shopping-cart"></i>
Ajouter au panier
</button>
</div>
</div>
</div>
<style>
.dropship-referral-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
}
.dropship-modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
}
.dropship-modal-content {
position: relative;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
z-index: 10001;
animation: dropshipModalSlideIn 0.3s ease-out;
}
@keyframes dropshipModalSlideIn {
from {
opacity: 0;
transform: translateY(-30px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.dropship-modal-close {
position: absolute;
top: 15px;
right: 15px;
background: none;
border: none;
font-size: 28px;
color: #666;
cursor: pointer;
width: 35px;
height: 35px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s;
z-index: 10;
}
.dropship-modal-close:hover {
background: #f0f0f0;
color: #333;
}
.dropship-modal-header {
padding: 30px 30px 20px;
text-align: center;
border-bottom: 1px solid #f0f0f0;
}
.dropship-icon {
width: 70px;
height: 70px;
margin: 0 auto 15px;
background: linear-gradient(135deg, #ffa200 0%, #ff8c00 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: white;
box-shadow: 0 4px 15px rgba(255, 162, 0, 0.3);
}
.dropship-modal-header h3 {
margin: 0 0 8px;
font-size: 22px;
font-weight: 600;
color: #333;
}
.dropship-subtitle {
margin: 0;
color: #666;
font-size: 14px;
}
.dropship-modal-body {
padding: 25px 30px;
}
.dropship-product-info {
display: flex;
gap: 15px;
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
}
.dropship-product-image {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 8px;
flex-shrink: 0;
}
.dropship-product-details {
flex: 1;
}
.dropship-product-details h4 {
margin: 0 0 8px;
font-size: 16px;
font-weight: 600;
color: #333;
}
.dropship-product-price {
margin-top: 5px;
}
.dropship-product-price .price {
font-size: 20px;
font-weight: 700;
color: #ffa200;
}
.dropship-message {
padding: 15px;
background: #fff9e6;
border-left: 4px solid #ffa200;
border-radius: 6px;
}
.dropship-message p {
margin: 0;
color: #666;
line-height: 1.6;
}
.dropship-modal-footer {
padding: 20px 30px 30px;
display: flex;
gap: 12px;
justify-content: flex-end;
border-top: 1px solid #f0f0f0;
}
.dropship-modal-footer .btn {
padding: 12px 24px;
border-radius: 8px;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.2s;
font-size: 15px;
}
.dropship-modal-footer .btn-secondary {
background: #f0f0f0;
color: #666;
}
.dropship-modal-footer .btn-secondary:hover {
background: #e0e0e0;
}
.dropship-modal-footer .btn-primary {
background: #ffa200;
color: white;
}
.dropship-modal-footer .btn-primary:hover {
background: #e8910a;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 162, 0, 0.3);
}
.dropship-modal-footer .btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
</style>
<script>
// Afficher le modal automatiquement au chargement de la page
document.addEventListener('DOMContentLoaded', function () {
const dropshipModal = document.getElementById('dropship-modal');
if (dropshipModal) {
setTimeout(function () {
dropshipModal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}, 500); // Délai de 500ms pour une meilleure UX
}
});
function closeDropshipModal () {
const dropshipModal = document.getElementById('dropship-modal');
if (dropshipModal) {
dropshipModal.style.display = 'none';
document.body.style.overflow = 'auto';
}
}
function addToCartFromDropship () {
const addBtn = document.querySelector('.dropship-add-to-cart');
if (! addBtn)
return;
// Désactiver le bouton pendant le traitement
addBtn.disabled = true;
addBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Ajout en cours...';
const productId = {{ product.id }};
const qty = parseInt(document.getElementById('sst') ?. value || '1');
// Utiliser fetch pour ajouter au panier
fetch('{{ path("cart_add") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams(
{productId: productId, qty: qty}
)
}).then(response => response.json()).then(data => {
if (data.ok) { // Afficher un message de succès
const modalBody = document.querySelector('.dropship-modal-body');
if (modalBody) {
modalBody.innerHTML = `
<div style="text-align: center; padding: 30px 20px;">
<div style="width: 80px; height: 80px; margin: 0 auto 20px; background: #d4edda; border-radius: 50%; display: flex; align-items: center; justify-content: center;">
<i class="fas fa-check" style="font-size: 40px; color: #28a745;"></i>
</div>
<h3 style="color: #28a745; margin-bottom: 10px;">Produit ajouté avec succès !</h3>
<p style="color: #666; margin-bottom: 20px;">Merci d'avoir utilisé le lien de recommandation de <strong>{{ dropshipReferral.affiliateName }}</strong></p>
<p style="color: #666; font-size: 14px;">Vous pouvez continuer vos achats ou aller directement au panier.</p>
</div>
`;
}
const modalFooter = document.querySelector('.dropship-modal-footer');
if (modalFooter) {
modalFooter.innerHTML = `
<button type="button" class="btn btn-secondary" onclick="closeDropshipModal()">
Continuer les achats
</button>
<a href="{{ path('cart') }}" class="btn btn-primary" style="text-decoration: none; display: inline-block;">
<i class="fas fa-shopping-cart"></i> Voir le panier
</a>
`;
}
// Mettre à jour le compteur du panier si présent
const cartCount = document.querySelector('.cart-count, .cart_count, [data-cart-count]');
if (cartCount) {
cartCount.textContent = data.totalQty || 0;
}
} else {
showCustomAlert('Erreur lors de l\'ajout au panier. Veuillez réessayer.', 'error');
addBtn.disabled = false;
addBtn.innerHTML = '<i class="fas fa-shopping-cart"></i> Ajouter au panier';
}
}).catch(error => {
console.error('Erreur:', error);
showCustomAlert('Une erreur est survenue. Veuillez réessayer.', 'error');
addBtn.disabled = false;
addBtn.innerHTML = '<i class="fas fa-shopping-cart"></i> Ajouter au panier';
});
}
// Fermer le modal en cliquant sur l'overlay
document.addEventListener('DOMContentLoaded', function () {
const dropshipModal = document.getElementById('dropship-modal');
if (dropshipModal) {
const overlay = dropshipModal.querySelector('.dropship-modal-overlay');
if (overlay) {
overlay.addEventListener('click', function () {
closeDropshipModal();
});
}
}
});
</script>
{% endif %}
<!-- Le Modal personnalisé et showCustomAlert ont été déplacés dans base_home.html.twig pour une portée globale -->
<script>
// Remplacer window.alert par showCustomAlert
window.alert = window.showCustomAlert || function(message) { console.log("Alert:", message); };
</script>
{% endblock %}