Files
ludikevent_crm/templates/revervation/produit.twig
Serreau Jovann afa6133907 ```
 feat(Product.php): Ajoute ProductDoc pour gérer les documents.
 feat(Contrats.php): Gère les fichiers du contrat via VichUploader.
 feat(templates): Crée template mail signature contrat.
 feat(SignatureController): Ajoute la signature du contrat.
 feat(ContratsController): Crée contrat depuis devis et liste contrats.
 feat(Client): Crée soumission contrat Docuseal.
 feat(DevisPdfService): Corrige l'assurance RC Pro.
 feat(.env): Ajoute CONTRAT_BASEURL.
 feat(ProductDocType): Crée formulaire pour les documents produit.
 feat(contrats/list.twig): Liste et actions pour les contrats.
 feat(UtmEvent.js): Track click document produit.
 feat(ContratEvent.php): Crée event pour envoi contrat.
 feat(admin.js): Initialise la recherche dynamique des contrats.
 feat(ContratPdfService): Génère le PDF du contrat DocuSeal.
 feat(products/add.twig): Ajoute gestion des documents produits.
 feat(ContratController): Crée controlleur contrat.
 feat(ContratSubscriber.php): Envoi du contrat par email.
 feat(reservation/produit.twig): Affiche les documents produit.
 feat(ProductController.php): Refactorisation et ajout des documents.
```
2026-01-22 15:58:57 +01:00

254 lines
15 KiB
Twig

{% extends 'revervation/base.twig' %}
{# --- SEO DYNAMIQUE & META-DONNÉES --- #}
{% block title %}{{ product.name }} - Location Ludikevent{% endblock %}
{% block description %}
Louez {{ product.name }} chez Ludikevent. Idéal pour les enfants ({{ product.category }}),
cette structure est disponible à partir de {{ product.priceDay }}€ la journée.
{{ product.description|striptags|slice(0, 150) }}...
Vérifiez la disponibilité en ligne !
{% endblock %}
{% block jsonld %}
<script type="application/ld+json">
{
"@context": "https://schema.org/",
"@type": "Product",
"name": "{{ product.name }}",
"image": [
"{% if product.imageName %}{{ absolute_url(vich_uploader_asset(product, 'imageFile')) }}{% else %}{{ absolute_url(asset('provider/images/favicon.png')) }}{% endif %}"
],
"description": "{{ product.description|striptags|slice(0, 160) }}",
"sku": "{{ product.ref }}",
"brand": {
"@type": "Brand",
"name": "Ludikevent"
},
"offers": {
"@type": "Offer",
"url": "{{ app.request.uri }}",
"priceCurrency": "EUR",
"price": "{{ product.priceDay }}",
"availability": "https://schema.org/InStock",
"itemCondition": "https://schema.org/UsedCondition",
"priceValidUntil": "{{ "now"|date_modify("+1 year")|date("Y-m-d") }}"
}
}
</script>
{% endblock %}
{% block breadcrumb_json %}
,{
"@type": "ListItem",
"position": 1,
"name": "Catalogue",
"item": "{{ absolute_url(path('reservation_catalogue')) }}"
},{
"@type": "ListItem",
"position": 2,
"name": "{{ product.name }}",
"item": "{{ absolute_url(path('reservation_product_show',{id:product.id})) }}"
}
{% endblock %}
{% block body %}
{# --- TRACKING ÉVÉNEMENT --- #}
<utm-event event="view_product" data="{{ product.json }}"></utm-event>
<div class="min-h-screen bg-white font-sans antialiased">
{# --- NAVIGATION / BREADCRUMB --- #}
<div class="max-w-7xl 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>
<a href="{{ url('reservation_catalogue') }}" class="text-slate-400 hover:text-blue-600 transition">Catalogue</a>
<span class="text-slate-300">/</span>
<span class="text-amber-500 underline decoration-2 underline-offset-4">{{ product.name }}</span>
</nav>
</div>
{# --- BOUTON RETOUR --- #}
<div class="max-w-7xl mx-auto px-4 pt-4">
<a href="{{ path('reservation_catalogue') }}" class="group inline-flex items-center gap-3 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 hover:text-blue-600 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 transform group-hover:-translate-x-2 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Retour au catalogue
</a>
</div>
<main class="max-w-7xl mx-auto px-4 py-12 md:py-20">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-16 md:gap-24 items-start">
{# --- COLONNE GAUCHE : VISUEL --- #}
<div class="sticky top-24">
<div class="relative overflow-hidden rounded-[4rem] bg-slate-50 aspect-[4/5] shadow-2xl">
{% if product.imageName %}
<img src="{{ vich_uploader_asset(product,'imageFile') | imagine_filter('webp') }}"
alt="{{ product.name }}"
class="w-full h-full object-cover">
{% else %}
<div class="h-full flex flex-col items-center justify-center p-12 text-center opacity-20">
<img src="{{ asset('provider/images/favicon.png') }}" alt="Ludik Event" class="w-40 h-40">
</div>
{% endif %}
<div class="absolute top-8 left-8">
<span class="bg-blue-600 text-white px-6 py-2 rounded-2xl text-[10px] font-black uppercase tracking-widest shadow-xl">
{{ product.category }}
</span>
</div>
</div>
</div>
{# --- COLONNE DROITE : CONTENU --- #}
<div class="flex flex-col h-full py-4">
<div class="mb-12">
<span class="text-[12px] font-black text-slate-300 uppercase tracking-[0.4em] mb-4 block italic">
Référence : {{ product.ref }}
</span>
<h1 class="text-6xl md:text-8xl font-black text-slate-900 uppercase italic tracking-tighter leading-[0.85] mb-10">
{{ product.name }}
</h1>
<div class="space-y-6">
{# Prix principal #}
<div class="flex items-baseline gap-4">
<span class="text-6xl font-black text-blue-600 ">{{ product.priceDay }}€</span>
<span class="text-sm font-bold text-slate-400 uppercase tracking-widest italic">La première journée</span>
</div>
{# Prix supplémentaire #}
<div class="flex items-center gap-3 bg-slate-50 self-start px-5 py-3 rounded-2xl border border-slate-100 w-fit">
<span class="text-2xl font-black text-slate-900 italic">+ {{ product.priceSup }}€</span>
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest italic">Par journée supplémentaire</span>
</div>
</div>
</div>
{# --- DESCRIPTION --- #}
<div class="prose prose-slate prose-lg max-w-none mb-12 text-slate-600 leading-relaxed">
{{ product.description|raw }}
</div>
<div class="border-t border-slate-100 pt-10 mb-12">
<div class="grid grid-cols-1 gap-8">
{# Badge Âge #}
<div class="bg-slate-50 p-6 rounded-[2rem] border border-slate-100 flex items-center justify-between">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Âge conseillé</span>
<span class="text-lg font-black text-slate-900 uppercase italic">{{ product.category }}</span>
</div>
{# SÉLECTEUR DE PÉRIODE --- #}
<div class="bg-blue-50/50 p-8 rounded-[3rem] border border-blue-100">
<span class="block text-[10px] font-black text-blue-600 uppercase tracking-[0.2em] mb-6 italic text-center">Planifiez votre événement</span>
<div class="flex flex-col sm:flex-row gap-4">
<div class="flex-1">
<label class="block text-[8px] font-black uppercase text-slate-400 mb-2 ml-4">Date de début</label>
<input type="date" class="w-full bg-white border-none rounded-2xl px-5 py-4 text-sm font-bold text-slate-900 focus:ring-2 focus:ring-blue-600 shadow-sm">
</div>
<div class="flex-1">
<label class="block text-[8px] font-black uppercase text-slate-400 mb-2 ml-4">Date de fin</label>
<input type="date" class="w-full bg-white border-none rounded-2xl px-5 py-4 text-sm font-bold text-slate-900 focus:ring-2 focus:ring-blue-600 shadow-sm">
</div>
</div>
</div>
</div>
</div>
{# --- ACTION FINALE --- #}
<div class="mt-auto">
<a href="{{ path('reservation_contact', {id: product.id}) }}"
class="flex items-center justify-center w-full py-8 bg-slate-900 text-white rounded-[2.5rem] font-black uppercase text-[12px] tracking-[0.3em] hover:bg-blue-600 transition-all shadow-2xl hover:scale-[1.02] active:scale-95">
Vérifier la disponibilité
</a>
<p class="text-center mt-6 text-[9px] font-black text-slate-300 uppercase tracking-widest italic">
Devis gratuit • Ludikevent • Qualité Pro
</p>
</div>
</div>
</div>
{# --- DOCUMENTS PUBLICS (NOTICES, PDF) --- #}
{% set publicDocs = product.productDocs|filter(doc => doc.isPublic) %}
{% if publicDocs|length > 0 %}
<div class="mt-12 mb-12">
<span class="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] mb-4 block italic">Ressources techniques</span>
<div class="space-y-3">
{% for doc in publicDocs %}
<div class="cta-file">
<utm-event event="click_pdf_product" data="{{ doc.json }}" ></utm-event>
<a href="{{ vich_uploader_asset(doc, 'docProduct') }}"
download="{{ doc.name }}.pdf"
class="flex items-center justify-between p-5 bg-white border border-slate-100 rounded-3xl hover:border-blue-500 hover:shadow-lg transition-all group">
<div class="flex items-center gap-4">
<div class="w-10 h-10 bg-red-50 text-red-500 rounded-xl flex items-center justify-center group-hover:bg-red-500 group-hover:text-white transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>
</svg>
</div>
<div>
<p class="text-[11px] font-black text-slate-900 uppercase italic tracking-wider">{{ doc.name }}</p>
</div>
</div>
<div class="text-slate-300 group-hover:text-blue-600 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
</div>
</a>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</main>
{# --- SECTION SUGGESTIONS (CROSS-SELLING) --- #}
<section class="max-w-7xl mx-auto px-4 py-24 border-t border-slate-100 mt-12">
<div class="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-16">
<div>
<span class="text-[10px] font-black text-blue-600 uppercase tracking-[0.3em] mb-3 block italic">Vous pourriez aussi aimer</span>
<h2 class="text-4xl md:text-6xl font-black text-slate-900 uppercase italic tracking-tighter leading-none">D'autres <span class="text-blue-600">idées ?</span></h2>
</div>
<a href="{{ path('reservation_catalogue') }}" class="inline-flex items-center gap-2 text-[10px] font-black uppercase tracking-widest border-b-2 border-slate-900 pb-1 hover:text-blue-600 hover:border-blue-600 transition-all">
Tout le catalogue
</a>
</div>
<div class="grid grid-cols-2 lg:grid-cols-4 gap-8">
{% for other in otherProducts %}
<div class="group transition-all duration-500">
<a href="{{ path('reservation_product_show', {id: (other.slug)}) }}" class="block">
<div class="relative overflow-hidden rounded-[2.5rem] bg-slate-50 aspect-square mb-6 shadow-sm group-hover:shadow-2xl transition-all duration-700">
{% if other.imageName %}
<img src="{{ vich_uploader_asset(other,'imageFile') | imagine_filter('webp') }}"
alt="{{ other.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 opacity-10">
<img src="{{ asset('provider/images/favicon.png') }}" class="w-16">
</div>
{% endif %}
<div class="absolute top-4 right-4 bg-white/95 backdrop-blur-md px-3 py-1.5 rounded-xl shadow-sm">
<p class="text-slate-900 font-black text-[12px] italic leading-none">{{ other.priceDay }}€</p>
</div>
</div>
<div class="px-2">
<span class="text-[8px] font-black text-slate-300 uppercase tracking-widest mb-1 block italic">{{ other.category }}</span>
<h3 class="text-lg font-black text-slate-900 uppercase italic tracking-tighter leading-tight group-hover:text-blue-600 transition-colors line-clamp-1">
{{ other.name }}
</h3>
</div>
</a>
</div>
{% else %}
<p class="col-span-full text-center text-slate-400 italic text-sm py-12">Plus de surprises arrivent bientôt...</p>
{% endfor %}
</div>
</section>
</div>
{% endblock %}