```
✨ feat(ReserverController): Ajoute les routes et vues pour le catalogue. ✨ feat(templates): Ajoute template catalogue et modifie base et contact. ✨ feat(assets): Ajoute loader turbo, filtre catalogue et améliore JS. ```
This commit is contained in:
@@ -1,27 +1,55 @@
|
||||
import './reserve.scss';
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import * as Turbo from "@hotwired/turbo"
|
||||
|
||||
// --- INITIALISATION SENTRY ---
|
||||
Sentry.init({
|
||||
dsn: "https://803814be6540031b1c37bf92ba9c0f79@sentry.esy-web.dev/24",
|
||||
tunnel: "/sentry-tunnel",
|
||||
sendDefaultPii: true,
|
||||
integrations: [
|
||||
Sentry.browserTracingIntegration(),
|
||||
Sentry.replayIntegration()
|
||||
],
|
||||
integrations: [Sentry.browserTracingIntegration()],
|
||||
tracesSampleRate: 1.0,
|
||||
tracePropagationTargets: ["localhost", "esy-web.dev", "revervation.ludikevent.fr"],
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1.0
|
||||
});
|
||||
|
||||
// --- LOGIQUE DU LOADER TURBO ---
|
||||
const initLoader = () => {
|
||||
let loaderEl = document.getElementById('turbo-loader');
|
||||
|
||||
if (!loaderEl) {
|
||||
loaderEl = document.createElement('div');
|
||||
loaderEl.id = 'turbo-loader';
|
||||
loaderEl.className = 'fixed inset-0 z-[9999] flex items-center justify-center bg-white transition-opacity duration-300 opacity-0 pointer-events-none';
|
||||
loaderEl.innerHTML = `
|
||||
<div class="relative flex items-center justify-center">
|
||||
<div class="absolute w-24 h-24 border-4 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
|
||||
<img src="/provider/images/favicon.png" class="w-12 h-12 relative z-10 animate-pulse" alt="Logo">
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(loaderEl);
|
||||
}
|
||||
|
||||
const showLoader = () => {
|
||||
loaderEl.classList.remove('opacity-0', 'pointer-events-none');
|
||||
loaderEl.classList.add('opacity-100');
|
||||
};
|
||||
|
||||
const hideLoader = () => {
|
||||
setTimeout(() => {
|
||||
loaderEl.classList.remove('opacity-100');
|
||||
loaderEl.classList.add('opacity-0', 'pointer-events-none');
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// --- ÉVÉNEMENTS TURBO (SANS BOUCLE INFINIE) ---
|
||||
document.addEventListener("turbo:click", showLoader);
|
||||
document.addEventListener("turbo:submit-start", showLoader);
|
||||
document.addEventListener("turbo:load", hideLoader);
|
||||
document.addEventListener("turbo:render", hideLoader);
|
||||
};
|
||||
|
||||
// --- LOGIQUE DU MENU MOBILE ---
|
||||
const initMobileMenu = () => {
|
||||
const btn = document.getElementById('menu-button');
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
|
||||
|
||||
if (btn && menu) {
|
||||
btn.onclick = () => {
|
||||
const isExpanded = btn.getAttribute('aria-expanded') === 'true';
|
||||
@@ -31,11 +59,59 @@ const initMobileMenu = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// --- LOGIQUE FILTRE CATALOGUE ---
|
||||
const initCatalogueSearch = () => {
|
||||
const filters = document.querySelectorAll('.filter-btn');
|
||||
const products = document.querySelectorAll('.product-item');
|
||||
const emptyMsg = document.getElementById('empty-msg');
|
||||
|
||||
if (!filters.length) return;
|
||||
|
||||
filters.forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
const category = btn.getAttribute('data-filter').toLowerCase();
|
||||
let count = 0;
|
||||
|
||||
// UI boutons
|
||||
filters.forEach(f => {
|
||||
f.classList.remove('bg-slate-900', 'text-white');
|
||||
f.classList.add('bg-white', 'text-slate-500');
|
||||
});
|
||||
btn.classList.add('bg-slate-900', 'text-white');
|
||||
btn.classList.remove('bg-white', 'text-slate-500');
|
||||
|
||||
// Filtrage
|
||||
products.forEach(item => {
|
||||
const itemCat = item.getAttribute('data-category').toLowerCase();
|
||||
if (category === 'all' || itemCat.includes(category)) {
|
||||
item.style.display = 'block';
|
||||
count++;
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
if (emptyMsg) {
|
||||
count === 0 ? emptyMsg.classList.remove('hidden') : emptyMsg.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// --- INITIALISATION ---
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initLoader();
|
||||
initMobileMenu();
|
||||
initCatalogueSearch();
|
||||
});
|
||||
|
||||
document.addEventListener('turbo:load', () => {
|
||||
initMobileMenu();
|
||||
initCatalogueSearch();
|
||||
});
|
||||
|
||||
// Nettoyage avant cache pour éviter les bugs au retour arrière
|
||||
document.addEventListener("turbo:before-cache", () => {
|
||||
document.querySelectorAll('.product-item').forEach(i => i.style.display = 'block');
|
||||
if (document.getElementById('empty-msg')) document.getElementById('empty-msg').classList.add('hidden');
|
||||
});
|
||||
|
||||
@@ -53,6 +53,20 @@ class ReserverController extends AbstractController
|
||||
return $this->render('revervation/home.twig',[
|
||||
'products' => $products
|
||||
]);
|
||||
}
|
||||
#[Route('/reservation/catalogue', name: 'reservation_catalogue')]
|
||||
public function revervationCatalogue(ProductRepository $productRepository): Response
|
||||
{
|
||||
|
||||
return $this->render('revervation/catalogue.twig',[
|
||||
'products' => $productRepository->findAll(),
|
||||
]);
|
||||
}
|
||||
#[Route('/reservation/produit/{id}', name: 'reservation_product_show')]
|
||||
public function revervationShowProduct(ProductRepository $productRepository): Response
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
#[Route('/reservation/contact', name: 'reservation_contact')]
|
||||
public function revervationContact(Request $request, Mailer $mailer): Response
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
{# Menu Desktop #}
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="{{ path('reservation') }}" class="text-gray-700 hover:text-blue-600 font-medium transition-colors">Accueil</a>
|
||||
<a href="{{ path('reservation_contact') }}" class="text-gray-700 hover:text-blue-600 font-medium transition-colors">Catalogue</a>
|
||||
<a href="{{ path('reservation_catalogue') }}" class="text-gray-700 hover:text-blue-600 font-medium transition-colors">Catalogue</a>
|
||||
<a href="{{ path('reservation_contact') }}" class="text-gray-700 hover:text-blue-600 font-medium transition-colors">Contact</a>
|
||||
|
||||
<a href="{{ path('reservation_search') }}" class="p-2 text-gray-500 hover:text-blue-600 transition-colors" aria-label="Rechercher">
|
||||
|
||||
140
templates/revervation/catalogue.twig
Normal file
140
templates/revervation/catalogue.twig
Normal file
@@ -0,0 +1,140 @@
|
||||
{% extends 'revervation/base.twig' %}
|
||||
|
||||
{% block title %}Catalogue Ludikevent - Location de structures gonflables{% endblock %}
|
||||
{% block breadcrumb_json %}
|
||||
,{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "Catalogue",
|
||||
"item": "{{ path('reservation_catalogue') }}"
|
||||
}
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<div class="min-h-screen bg-gray-50/50 font-sans antialiased pb-20">
|
||||
|
||||
{# --- HEADER --- #}
|
||||
<div class="max-w-[1600px] mx-auto pt-16 pb-8 px-4 text-center">
|
||||
<nav class="flex justify-center space-x-4 text-[10px] mb-8 uppercase tracking-[0.3em] font-black italic">
|
||||
<a href="{{ url('reservation') }}" class="text-slate-400 hover:text-blue-600 transition">ACCUEIL</a>
|
||||
<span class="text-slate-300">/</span>
|
||||
<span class="text-amber-500 underline decoration-2 underline-offset-4">Catalogue</span>
|
||||
</nav>
|
||||
|
||||
<h1 class="text-5xl md:text-7xl font-black text-slate-900 uppercase tracking-tighter italic leading-none mb-6">
|
||||
Tout notre <span class="text-blue-600">Univers</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{# --- BARRE DE RECHERCHE / FILTRES (STICKY) --- #}
|
||||
<div class="sticky top-0 z-40 bg-gray-50/90 backdrop-blur-md border-b border-slate-200 mb-12">
|
||||
<div class="max-w-[1600px] mx-auto px-4 py-4">
|
||||
<div class="flex flex-wrap justify-center gap-2 md:gap-3">
|
||||
|
||||
{# Bouton Tout voir #}
|
||||
<button data-filter="all" class="filter-btn px-5 py-2.5 rounded-xl font-black italic text-[9px] tracking-widest transition-all uppercase shadow-sm bg-slate-900 text-white border border-slate-900">
|
||||
Tout voir
|
||||
</button>
|
||||
|
||||
{# Liste des catégories en dur #}
|
||||
{% set categories_list = [
|
||||
{'id': '3-15 ans', 'label': '3-15 ANS', 'hover': 'hover:border-blue-600 hover:text-blue-600'},
|
||||
{'id': '2-7 ans', 'label': '2-7 ANS', 'hover': 'hover:border-amber-500 hover:text-amber-500'},
|
||||
{'id': '3-99 ans', 'label': '3-99 ANS', 'hover': 'hover:border-indigo-600 hover:text-indigo-600'},
|
||||
{'id': 'barnums', 'label': 'BARNUMS', 'hover': 'hover:border-slate-800 hover:text-slate-800'},
|
||||
{'id': 'alimentaire', 'label': 'ALIMENTAIRE', 'hover': 'hover:border-rose-500 hover:text-rose-500'},
|
||||
{'id': 'options', 'label': 'OPTIONS', 'hover': 'hover:border-emerald-500 hover:text-emerald-500'}
|
||||
] %}
|
||||
|
||||
{% for cat in categories_list %}
|
||||
<button data-filter="{{ cat.id }}"
|
||||
class="filter-btn px-5 py-2.5 rounded-xl font-black italic text-[9px] tracking-widest transition-all uppercase bg-white text-slate-400 border border-slate-200 {{ cat.hover }}">
|
||||
{{ cat.label }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- GRILLE DE PRODUITS (5 COLONNES) --- #}
|
||||
<div class="max-w-[1600px] mx-auto px-4">
|
||||
<div id="product-grid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-x-6 gap-y-12">
|
||||
|
||||
{% for product in products %}
|
||||
<div class="product-item group transition-all duration-500" data-category="{{ product.category|lower }}">
|
||||
<a href="{{ path('reservation_product_show', {id: product.id}) }}" class="block">
|
||||
|
||||
{# Image avec arrondi et badge prix #}
|
||||
<div class="relative overflow-hidden rounded-[2.5rem] bg-slate-100 aspect-square mb-4 shadow-sm group-hover:shadow-2xl transition-all duration-700">
|
||||
{% if product.imageName %}
|
||||
<img src="{{ vich_uploader_asset(product,'imageFile') | imagine_filter('webp') }}"
|
||||
alt="{{ product.name }}"
|
||||
class="w-full h-full object-cover transform group-hover:scale-110 transition-transform duration-1000">
|
||||
{% else %}
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<img src="{{ asset('provider/images/favicon.png') }}" class="w-10 h-10 opacity-10">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Badge Prix #}
|
||||
<div class="absolute top-3 right-3 bg-white/95 backdrop-blur-md px-3 py-1.5 rounded-xl shadow-sm border border-slate-100">
|
||||
<p class="text-slate-900 font-black text-[12px] italic leading-none">
|
||||
{{ product.priceDay }}€
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Informations produit #}
|
||||
<div class="px-2">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="text-[7px] font-black text-blue-600 uppercase tracking-widest italic">
|
||||
{{ product.category }}
|
||||
</span>
|
||||
<span class="text-[7px] font-bold text-slate-300 uppercase tracking-tighter">
|
||||
REF. {{ product.ref }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 class="text-sm font-black text-slate-900 uppercase italic tracking-tighter leading-tight group-hover:text-blue-600 transition-colors line-clamp-2">
|
||||
{{ product.name }}
|
||||
</h3>
|
||||
|
||||
<div class="mt-3 flex items-center gap-1 text-[8px] font-black uppercase text-slate-400 group-hover:text-blue-600 transition-colors">
|
||||
<span>Voir le produit</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 transform group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{# Message si catégorie vide #}
|
||||
<div id="empty-msg" class="hidden col-span-full py-24 text-center bg-white rounded-[3.5rem] border-2 border-dashed border-slate-100">
|
||||
<div class="text-4xl mb-4">🎈</div>
|
||||
<p class="text-slate-400 font-black italic uppercase tracking-widest text-xs">
|
||||
Arrive bientôt dans cette catégorie...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- CTA FINAL --- #}
|
||||
<div class="max-w-[1600px] mx-auto px-4 mt-32">
|
||||
<div class="bg-slate-900 rounded-[3.5rem] p-12 text-center relative overflow-hidden shadow-2xl">
|
||||
<div class="relative z-10">
|
||||
<h2 class="text-3xl md:text-5xl font-black text-white uppercase italic tracking-tighter">Vous ne trouvez pas <span class="text-blue-500">votre bonheur ?</span></h2>
|
||||
<p class="text-slate-400 mt-4 max-w-xl mx-auto font-medium italic text-sm">Contactez Lilian directement pour une demande sur mesure ou des conseils personnalisés.</p>
|
||||
<div class="mt-8">
|
||||
<a href="{{ path('reservation_contact') }}" class="inline-block px-10 py-5 bg-blue-600 text-white rounded-2xl font-black uppercase tracking-widest hover:bg-white hover:text-slate-900 transition-all shadow-xl">
|
||||
Nous appeler ⚡
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute -right-20 -bottom-20 w-80 h-80 bg-blue-600/10 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,12 +1,25 @@
|
||||
{% extends 'revervation/base.twig' %}
|
||||
|
||||
{% block title %}Contactez l'expert - Ludik Event{% endblock %}
|
||||
|
||||
{% block breadcrumb_json %}
|
||||
,{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "Nous contact",
|
||||
"item": "{{ path('reservation_contact') }}"
|
||||
}
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<div class="min-h-screen bg-gray-50/50 font-sans antialiased pb-20">
|
||||
|
||||
{# --- HEADER --- #}
|
||||
<div class="max-w-6xl mx-auto pt-16 pb-12 px-4">
|
||||
<nav class="flex justify-center space-x-2 text-xs mb-8 uppercase tracking-[0.2em] font-black">
|
||||
<a href="{{ url('reservation') }}" class="text-slate-900 hover:text-indigo-600 transition">ACCUEIL</a>
|
||||
<span class="text-slate-300">/</span>
|
||||
<span class="text-amber-500">Contact</span>
|
||||
</nav>
|
||||
|
||||
<div class="flex flex-col md:flex-row items-end justify-between gap-6 border-b border-slate-200 pb-8">
|
||||
<div>
|
||||
<h1 class="text-5xl md:text-7xl font-black text-slate-900 uppercase tracking-tighter italic leading-none">
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
Ludik Event simplifie vos événements. Des structures certifiées, une livraison rapide et des souvenirs inoubliables pour les petits et les grands.
|
||||
</p>
|
||||
<div class="mt-10 flex flex-col sm:flex-row gap-4 sm:justify-center lg:justify-start">
|
||||
<a href="#catalogue" class="px-8 py-4 bg-blue-600 text-white rounded-2xl font-bold text-lg shadow-xl shadow-blue-200 hover:bg-blue-700 transition-all hover:-translate-y-1 text-center uppercase tracking-tighter">
|
||||
<a href="{{ path('reservation_catalogue') }}" class="px-8 py-4 bg-blue-600 text-white rounded-2xl font-bold text-lg shadow-xl shadow-blue-200 hover:bg-blue-700 transition-all hover:-translate-y-1 text-center uppercase tracking-tighter">
|
||||
Voir le catalogue
|
||||
</a>
|
||||
<a href="tel:0614172447" class="px-8 py-4 bg-white text-gray-700 border border-gray-200 rounded-2xl font-bold text-lg hover:bg-gray-50 transition-all text-center uppercase tracking-tighter">
|
||||
|
||||
Reference in New Issue
Block a user