{# * Product Gallery Page - 5sur5séjour * Clean rewrite with self-contained
styles * Original purpose: Display products with image galleries and ordering
capability #} {% extends 'Accueil/layoutAccueil.html.twig' %} {% block
stylesheets %}
{{ parent() }}
<style>
/* ============================================
5sur5séjour Product Gallery - Clean Styles
============================================ */
:root {
--primary: #41a2aa;
--primary-dark: #359ba3;
--primary-light: rgba(65, 162, 170, 0.1);
--orange: #f56040;
--text-dark: #1a1a1a;
--text-muted: #6b7280;
--bg-light: #f8fafb;
--white: #ffffff;
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
--shadow-md: 0 4px 20px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.15);
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 20px;
}
/* Page Container */
.product-page {
background: var(--bg-light);
min-height: 100vh;
padding: 40px 20px;
}
.product-container {
max-width: 1200px;
margin: 0 auto;
}
/* Login Modal */
.login-modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 10000;
align-items: center;
justify-content: center;
}
.login-modal-overlay.active {
display: flex;
}
.login-modal {
background: var(--white);
border-radius: var(--radius-lg);
padding: 32px;
max-width: 400px;
width: 90%;
text-align: center;
box-shadow: var(--shadow-lg);
}
.login-modal h3 {
color: var(--text-dark);
font-size: 20px;
font-weight: 600;
margin-bottom: 24px;
}
.login-modal-close {
position: absolute;
top: 16px;
right: 16px;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--text-muted);
}
/* Product Card */
.product-card {
display: none;
background: var(--white);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
overflow: hidden;
margin-bottom: 30px;
}
.product-card.active {
display: block;
}
.product-card-inner {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
}
/* Gallery Section */
.product-gallery {
padding: 30px;
display: flex;
flex-direction: column;
align-items: center;
}
.main-image-container {
position: relative;
width: 100%;
max-width: 500px;
margin-bottom: 20px;
}
.main-image {
width: 100%;
height: auto;
border-radius: var(--radius-md);
box-shadow: var(--shadow-md);
transition: transform 0.3s ease, opacity 0.2s ease;
cursor: zoom-in;
}
.main-image:hover {
transform: scale(1.02);
}
.thumbnails {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
max-width: 500px;
}
.thumbnail {
width: 70px;
height: 70px;
border-radius: var(--radius-sm);
object-fit: cover;
cursor: pointer;
border: 3px solid transparent;
transition: all 0.3s ease;
box-shadow: var(--shadow-sm);
}
.thumbnail:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-md);
}
.thumbnail.active {
border-color: var(--primary);
}
/* Product Info Section */
.product-info {
padding: 40px;
display: flex;
flex-direction: column;
}
.product-title {
font-size: 28px;
font-weight: 700;
color: var(--text-dark);
margin-bottom: 24px;
line-height: 1.3;
}
/* Feature Blocks */
.feature-block {
background: var(--primary-light);
border-radius: var(--radius-md);
padding: 20px;
margin-bottom: 16px;
border: 1px solid rgba(65, 162, 170, 0.15);
}
.feature-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.feature-icon {
width: 24px;
height: 24px;
flex-shrink: 0;
}
.feature-title {
font-size: 16px;
font-weight: 600;
color: var(--text-dark);
margin: 0;
}
.feature-content {
font-size: 14px;
color: var(--text-muted);
line-height: 1.7;
margin: 0;
}
/* Order Button */
.order-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
background: linear-gradient(
135deg,
var(--primary) 0%,
var(--primary-dark) 100%
);
color: var(--white);
border: none;
border-radius: var(--radius-md);
padding: 16px 32px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 14px rgba(65, 162, 170, 0.35);
margin-top: auto;
width: fit-content;
}
.order-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(65, 162, 170, 0.45);
}
.order-btn:active {
transform: translateY(0);
}
/* Navigation Controls */
.product-nav {
display: flex;
justify-content: center;
gap: 16px;
margin-top: 20px;
}
.nav-btn {
width: 50px;
height: 50px;
border-radius: 50%;
background: var(--white);
border: none;
box-shadow: var(--shadow-md);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.nav-btn:hover {
transform: scale(1.1);
box-shadow: var(--shadow-lg);
}
.nav-btn svg {
width: 20px;
height: 20px;
fill: var(--text-dark);
}
/* Product Dots Navigation */
.product-dots {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 16px;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #ddd;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.dot.active {
background: var(--primary);
transform: scale(1.2);
}
.dot:hover {
background: var(--primary-dark);
}
/* ============================================
Mobile Styles
============================================ */
.mobile-slider {
display: none;
}
@media (max-width: 991px) {
.product-card-inner {
grid-template-columns: 1fr;
}
.product-gallery {
padding: 20px;
}
.product-info {
padding: 24px;
}
.product-title {
font-size: 22px;
}
}
@media (max-width: 768px) {
.product-page {
padding: 20px 12px;
}
.desktop-view {
display: none;
}
.mobile-slider {
display: block;
}
.mobile-product-card {
background: var(--white);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
overflow: hidden;
margin-bottom: 20px;
}
.mobile-main-image {
width: 100%;
height: auto;
display: block;
}
.mobile-thumbnails {
display: flex;
gap: 8px;
padding: 12px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.mobile-thumbnail {
width: 60px;
height: 60px;
border-radius: var(--radius-sm);
object-fit: cover;
flex-shrink: 0;
cursor: pointer;
border: 2px solid transparent;
transition: border-color 0.3s ease;
}
.mobile-thumbnail.active {
border-color: var(--primary);
}
.mobile-product-info {
padding: 20px;
}
.mobile-product-title {
font-size: 20px;
font-weight: 700;
color: var(--text-dark);
margin-bottom: 16px;
}
.feature-block {
padding: 16px;
margin-bottom: 12px;
}
.feature-title {
font-size: 15px;
}
.feature-content {
font-size: 13px;
}
.order-btn {
width: 100%;
padding: 14px 24px;
}
.thumbnail {
width: 55px;
height: 55px;
}
}
/* Slick Slider Overrides for Mobile */
.mobile-slider .slick-slide {
padding: 0 5px;
}
.mobile-slider .slick-dots {
bottom: -30px;
}
.mobile-slider .slick-dots li button:before {
color: var(--primary);
}
.mobile-slider .slick-prev,
.mobile-slider .slick-next {
z-index: 10;
}
.mobile-slider .slick-prev {
left: 10px;
}
.mobile-slider .slick-next {
right: 10px;
}
/* Image Loading States */
.main-image.loading {
opacity: 0.6;
}
/* Lightbox (optional enhancement) */
.lightbox-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.9);
z-index: 10001;
align-items: center;
justify-content: center;
cursor: zoom-out;
}
.lightbox-overlay.active {
display: flex;
}
.lightbox-image {
max-width: 90%;
max-height: 90%;
border-radius: var(--radius-md);
}
.lightbox-close {
position: absolute;
top: 20px;
right: 20px;
background: var(--white);
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
font-size: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
</style>
{% endblock %} {% block Header %} {% if app.user %}
{{
render(controller("App\\Controller\\EspaceParentController::headerparenrt"))
}}
{% else %}
{{ render(controller("App\\Controller\\AccueilController::header")) }}
{% endif %} {% endblock %} {% block Content %}
<div class="product-page">
<div class="product-container">
<!-- Login Required Modal -->
<div class="login-modal-overlay" id="loginModal">
<div class="login-modal">
<h3>Veuillez d'abord vous connecter !</h3>
<a href="/" class="order-btn">OK</a>
</div>
</div>
<!-- Lightbox for Image Zoom -->
<div class="lightbox-overlay" id="lightbox" onclick="closeLightbox()">
<button class="lightbox-close" onclick="closeLightbox()">×</button>
<img class="lightbox-image" id="lightboxImage" src="" alt="" />
</div>
<!-- Desktop View -->
<div class="desktop-view">
{% for product in produit %} {% if product.labeleType != "Connexion" %}
<div
class="product-card"
id="product-{{ loop.index }}"
data-product-id="{{ product.labeleType|replace({' ': ''}) }}"
{%
if
loop.first
%}style="display: block;"
{%
endif
%}
>
<div class="product-card-inner">
<!-- Gallery -->
<div class="product-gallery">
<div class="main-image-container">
<img
class="main-image"
id="mainImage-{{ loop.index }}"
src="{{ (product.attachements[0].idAttachement.path)|replace({'/upload/': '/upload/f_auto,q_auto,w_800/'}) }}"
alt="{{ product.labeleType }}"
onclick="openLightbox(this.src)"
loading="eager"
/>
</div>
<div class="thumbnails">
{% for attachment in product.attachements %} {% if
attachment.statut == "pageProduit" %}
<img
class="thumbnail {% if loop.first %}active{% endif %}"
src="{{ attachment.idAttachement.path|replace({'/upload/': '/upload/f_auto,q_auto,w_150,h_150,c_fill/'}) }}"
data-full="{{ attachment.idAttachement.path|replace({'/upload/': '/upload/f_auto,q_auto,w_800/'}) }}"
alt="{{ product.labeleType }} - Photo {{ loop.index }}"
onclick="changeMainImage(this, 'mainImage-{{
loop.parent.loop.index
}}')"
loading="lazy"
/>
{% endif %} {% endfor %}
</div>
</div>
<!-- Product Info -->
<div class="product-info">
<h2 class="product-title">{{ product.labeleType }}</h2>
<!-- Caractéristiques -->
<div class="feature-block">
<div class="feature-header">
<img
class="feature-icon"
src="{{ asset('images/SVG/5sur5-caracteristiques.svg') }}"
alt=""
/>
<h4 class="feature-title">Caractéristiques produit</h4>
</div>
<p class="feature-content">
{% if product.description %}{{ product.description | nl2br }}{%
endif %}
</p>
</div>
<!-- Les Plus -->
<div class="feature-block">
<div class="feature-header">
<img
class="feature-icon"
src="{{ asset('images/LesPlus5sur5.svg') }}"
alt=""
/>
<h4 class="feature-title">Les plus produit</h4>
</div>
<p class="feature-content">
{{ product.plusDescription | nl2br }}
</p>
</div>
<!-- Tarifs -->
<div class="feature-block">
<div class="feature-header">
<img
class="feature-icon"
src="{{ asset('images/SVG/5sur5-tarifs.svg') }}"
alt=""
/>
<h4 class="feature-title">Tarif & Frais de port</h4>
</div>
<p class="feature-content">{{ product.tarifs | nl2br }}</p>
</div>
<!-- Order Button -->
{% if app.user %} {% if is_granted('ROLE_PARENT') %}
<button
class="order-btn"
onclick="location.href='{{ path('projet-Parent') }}'"
>
Commander
</button>
{% elseif is_granted('ROLE_ACC') %}
<button
class="order-btn"
onclick="location.href='{{
path('ProjetsAccompagnateur', { id: sejour.id })
}}'"
>
Commander
</button>
{% endif %} {% else %}
<button class="order-btn" onclick="showLoginModal()">
Commander
</button>
{% endif %}
</div>
</div>
</div>
{% endif %} {% endfor %}
<!-- Navigation -->
{% set productCount = 0 %} {% for product in produit %} {% if
product.labeleType != "Connexion" %} {% set productCount = productCount +
1 %} {% endif %} {% endfor %} {% if productCount > 1 %}
<div class="product-nav">
<button
class="nav-btn"
onclick="navigateProduct(-1)"
aria-label="Produit précédent"
>
<svg viewBox="0 0 24 24">
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
</svg>
</button>
<button
class="nav-btn"
onclick="navigateProduct(1)"
aria-label="Produit suivant"
>
<svg viewBox="0 0 24 24">
<path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" />
</svg>
</button>
</div>
<div class="product-dots">
{% set dotIndex = 0 %} {% for product in produit %} {% if
product.labeleType != "Connexion" %}
<button
class="dot {% if dotIndex == 0 %}active{% endif %}"
onclick="goToProduct({{ dotIndex }})"
aria-label="Voir {{ product.labeleType }}"
></button>
{% set dotIndex = dotIndex + 1 %} {% endif %} {% endfor %}
</div>
{% endif %}
</div>
<!-- Mobile View -->
<div class="mobile-slider">
{% for product in produit %} {% if product.labeleType != "Connexion" %}
<div class="mobile-product-card">
<img
class="mobile-main-image"
id="mobileMainImage-{{ loop.index }}"
src="{{ (product.attachements[0].idAttachement.path)|replace({'/upload/': '/upload/f_auto,q_auto,w_600/'}) }}"
alt="{{ product.labeleType }}"
loading="lazy"
/>
<div class="mobile-thumbnails">
{% for attachment in product.attachements %} {% if attachment.statut
== "pageProduit" %}
<img
class="mobile-thumbnail {% if loop.first %}active{% endif %}"
src="{{ attachment.idAttachement.path|replace({'/upload/': '/upload/f_auto,q_auto,w_120,h_120,c_fill/'}) }}"
data-full="{{ attachment.idAttachement.path|replace({'/upload/': '/upload/f_auto,q_auto,w_600/'}) }}"
alt="{{ product.labeleType }} - Photo {{ loop.index }}"
onclick="changeMobileImage(this, 'mobileMainImage-{{
loop.parent.loop.index
}}')"
loading="lazy"
/>
{% endif %} {% endfor %}
</div>
<div class="mobile-product-info">
<h2 class="mobile-product-title">{{ product.labeleType }}</h2>
<div class="feature-block">
<div class="feature-header">
<img
class="feature-icon"
src="{{ asset('images/SVG/5sur5-caracteristiques.svg') }}"
alt=""
/>
<h4 class="feature-title">Caractéristiques produit</h4>
</div>
<p class="feature-content">
{% if product.description %}{{ product.description | nl2br }}{%
endif %}
</p>
</div>
<div class="feature-block">
<div class="feature-header">
<img
class="feature-icon"
src="{{ asset('images/LesPlus5sur5.svg') }}"
alt=""
/>
<h4 class="feature-title">Les plus produit</h4>
</div>
<p class="feature-content">{{ product.plusDescription | nl2br }}</p>
</div>
<div class="feature-block">
<div class="feature-header">
<img
class="feature-icon"
src="{{ asset('images/SVG/5sur5-tarifs.svg') }}"
alt=""
/>
<h4 class="feature-title">Tarif & Frais de port</h4>
</div>
<p class="feature-content">{{ product.tarifs | nl2br }}</p>
</div>
{% if app.user %} {% if is_granted('ROLE_PARENT') %}
<button
class="order-btn"
onclick="location.href='{{ path('projet-Parent') }}'"
>
Commander
</button>
{% elseif is_granted('ROLE_ACC') %}
<button
class="order-btn"
onclick="location.href='{{
path('ProjetsAccompagnateur', { id: sejour.id })
}}'"
>
Commander
</button>
{% endif %} {% else %}
<button class="order-btn" onclick="showLoginModal()">
Commander
</button>
{% endif %}
</div>
</div>
{% endif %} {% endfor %}
</div>
</div>
</div>
{% endblock %} {% block Footer %}
{{ parent() }}
{% endblock %} {% block javascript %}
{{ parent() }}
<script>
// ============================================
// Product Gallery - JavaScript
// ============================================
let currentProductIndex = 0;
const productCards = document.querySelectorAll('.desktop-view .product-card');
const dots = document.querySelectorAll('.dot');
const totalProducts = productCards.length;
// Initialize based on URL parameter or showArt variable
document.addEventListener('DOMContentLoaded', function() {
{% if showArt is defined and showArt != "ALL" %}
const targetId = "{{ showArt|replace({' ': ''}) }}";
productCards.forEach((card, index) => {
if (card.dataset.productId === targetId) {
goToProduct(index);
}
});
{% endif %}
// Initialize mobile slider if Slick is available
if (typeof $.fn.slick !== 'undefined') {
$('.mobile-slider').slick({
slidesToShow: 1,
slidesToScroll: 1,
autoplay: true,
autoplaySpeed: 4000,
dots: true,
arrows: false,
adaptiveHeight: true
});
}
});
// Navigate between products
function navigateProduct(direction) {
currentProductIndex += direction;
if (currentProductIndex >= totalProducts) {
currentProductIndex = 0;
} else if (currentProductIndex < 0) {
currentProductIndex = totalProducts - 1;
}
updateProductDisplay();
}
// Go to specific product
function goToProduct(index) {
currentProductIndex = index;
updateProductDisplay();
}
// Update display
function updateProductDisplay() {
productCards.forEach((card, index) => {
card.style.display = index === currentProductIndex ? 'block' : 'none';
});
dots.forEach((dot, index) => {
dot.classList.toggle('active', index === currentProductIndex);
});
}
// Change main image from thumbnail (Desktop)
function changeMainImage(thumbnail, mainImageId) {
const mainImage = document.getElementById(mainImageId);
const container = thumbnail.closest('.product-gallery');
// Update active state
container.querySelectorAll('.thumbnail').forEach(t => t.classList.remove('active'));
thumbnail.classList.add('active');
// Animate transition
mainImage.classList.add('loading');
mainImage.style.transform = 'scale(0.98)';
setTimeout(() => {
mainImage.src = thumbnail.dataset.full;
mainImage.onload = function() {
mainImage.classList.remove('loading');
mainImage.style.transform = 'scale(1)';
};
}, 150);
}
// Change main image from thumbnail (Mobile)
function changeMobileImage(thumbnail, mainImageId) {
const mainImage = document.getElementById(mainImageId);
const container = thumbnail.closest('.mobile-product-card');
container.querySelectorAll('.mobile-thumbnail').forEach(t => t.classList.remove('active'));
thumbnail.classList.add('active');
mainImage.style.opacity = '0.7';
setTimeout(() => {
mainImage.src = thumbnail.dataset.full;
mainImage.onload = function() {
mainImage.style.opacity = '1';
};
}, 150);
}
// Lightbox functions
function openLightbox(src) {
const lightbox = document.getElementById('lightbox');
const lightboxImage = document.getElementById('lightboxImage');
lightboxImage.src = src;
lightbox.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeLightbox() {
const lightbox = document.getElementById('lightbox');
lightbox.classList.remove('active');
document.body.style.overflow = '';
}
// Login modal
function showLoginModal() {
document.getElementById('loginModal').classList.add('active');
}
function hideLoginModal() {
document.getElementById('loginModal').classList.remove('active');
}
// Close modal on overlay click
document.getElementById('loginModal').addEventListener('click', function(e) {
if (e.target === this) {
hideLoginModal();
}
});
// Keyboard navigation
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeLightbox();
hideLoginModal();
} else if (e.key === 'ArrowLeft') {
navigateProduct(-1);
} else if (e.key === 'ArrowRight') {
navigateProduct(1);
}
});
</script>
{% endblock %}