```
✨ feat(Product): Ajoute description et quantité aux produits, et formulaire associé.
```
This commit is contained in:
32
migrations/Version20260121132344.php
Normal file
32
migrations/Version20260121132344.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260121132344 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE product ADD description TEXT DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE product DROP description');
|
||||||
|
}
|
||||||
|
}
|
||||||
32
migrations/Version20260121132406.php
Normal file
32
migrations/Version20260121132406.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20260121132406 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE product ADD qt INT DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE SCHEMA public');
|
||||||
|
$this->addSql('ALTER TABLE product DROP qt');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use App\Repository\ProductRepository;
|
|||||||
use Cocur\Slugify\Slugify;
|
use Cocur\Slugify\Slugify;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Component\HttpFoundation\File\File;
|
use Symfony\Component\HttpFoundation\File\File;
|
||||||
use Vich\UploaderBundle\Mapping\Attribute\Uploadable;
|
use Vich\UploaderBundle\Mapping\Attribute\Uploadable;
|
||||||
@@ -68,6 +69,12 @@ class Product
|
|||||||
#[ORM\OneToMany(targetEntity: ProductReserve::class, mappedBy: 'product')]
|
#[ORM\OneToMany(targetEntity: ProductReserve::class, mappedBy: 'product')]
|
||||||
private Collection $productReserves;
|
private Collection $productReserves;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
|
private ?string $description = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?int $qt = null;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->devisLines = new ArrayCollection();
|
$this->devisLines = new ArrayCollection();
|
||||||
@@ -285,4 +292,28 @@ class Product
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDescription(): ?string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(?string $description): static
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getQt(): ?int
|
||||||
|
{
|
||||||
|
return $this->qt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setQt(?int $qt): static
|
||||||
|
{
|
||||||
|
$this->qt = $qt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
|||||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TelType;
|
use Symfony\Component\Form\Extension\Core\Type\TelType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
@@ -27,9 +28,21 @@ class ProductType extends AbstractType
|
|||||||
'label' => 'Reference du produit',
|
'label' => 'Reference du produit',
|
||||||
'required' => true,
|
'required' => true,
|
||||||
])
|
])
|
||||||
->add('category',TextType::class,[
|
->add('description',TextareaType::class,[
|
||||||
|
'label' => 'Description du produit',
|
||||||
|
'required' => false,
|
||||||
|
])
|
||||||
|
->add('category',ChoiceType::class,[
|
||||||
'label' => 'Catégorie du produit',
|
'label' => 'Catégorie du produit',
|
||||||
'required' => true,
|
'required' => true,
|
||||||
|
'choices' => [
|
||||||
|
'2-7 Ans' =>'2-7 ans',
|
||||||
|
'3-15 Ans' =>'3-15 ans',
|
||||||
|
'3-99 Ans' =>'3-99 ans',
|
||||||
|
'Barnums' =>'barnums',
|
||||||
|
'Alimentaire' =>'alimentaire',
|
||||||
|
'Options' =>'options',
|
||||||
|
]
|
||||||
])
|
])
|
||||||
->add('caution',NumberType::class,[
|
->add('caution',NumberType::class,[
|
||||||
'label' => 'Caution du produit',
|
'label' => 'Caution du produit',
|
||||||
|
|||||||
@@ -73,6 +73,21 @@
|
|||||||
{{ form_widget(form.ref, {'attr': {'placeholder': 'REF-000', 'class': 'w-full bg-slate-900/50 border-white/5 rounded-2xl text-white font-mono focus:ring-blue-500/20 focus:border-blue-500 transition-all py-3.5 px-5'}}) }}
|
{{ form_widget(form.ref, {'attr': {'placeholder': 'REF-000', 'class': 'w-full bg-slate-900/50 border-white/5 rounded-2xl text-white font-mono focus:ring-blue-500/20 focus:border-blue-500 transition-all py-3.5 px-5'}}) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{# À placer juste après le bloc Référence Interne #}
|
||||||
|
<div class="md:col-span-2 mt-6">
|
||||||
|
{{ form_label(form.description, 'Description détaillée', {'label_attr': {'class': 'text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] ml-1 mb-2 block'}}) }}
|
||||||
|
{{ form_widget(form.description, {
|
||||||
|
'attr': {
|
||||||
|
'class': 'w-full bg-slate-900/50 border-white/5 rounded-2xl text-white focus:ring-blue-500/20 focus:border-blue-500 transition-all py-3.5 px-5 min-h-[150px]',
|
||||||
|
'placeholder': 'Décrivez les dimensions, la capacité, les points forts...'
|
||||||
|
}
|
||||||
|
}) }}
|
||||||
|
{% if form_errors(form.description) %}
|
||||||
|
<div class="text-rose-500 text-[10px] font-bold mt-2 ml-2 uppercase tracking-widest">
|
||||||
|
{{ form_errors(form.description) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,8 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{% set categories_list = [
|
{% 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': '2-7 ans', 'label': '2-7 ANS', 'hover': 'hover:border-amber-500 hover:text-amber-500'},
|
||||||
|
{'id': '3-15 ans', 'label': '3-15 ANS', 'hover': 'hover:border-blue-600 hover:text-blue-600'},
|
||||||
{'id': '3-99 ans', 'label': '3-99 ANS', 'hover': 'hover:border-indigo-600 hover:text-indigo-600'},
|
{'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': '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': 'alimentaire', 'label': 'ALIMENTAIRE', 'hover': 'hover:border-rose-500 hover:text-rose-500'},
|
||||||
|
|||||||
@@ -155,7 +155,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<a href="#" class="block w-full py-4 bg-gray-900 text-white text-center rounded-[1.5rem] font-black uppercase text-sm tracking-widest hover:bg-blue-600 transition-all shadow-xl hover:shadow-blue-200 active:scale-95">
|
<a href="{{ path('reservation_product_show',{id:product.slug}) }}" class="block w-full py-4 bg-gray-900 text-white text-center rounded-[1.5rem] font-black uppercase text-sm tracking-widest hover:bg-blue-600 transition-all shadow-xl hover:shadow-blue-200 active:scale-95">
|
||||||
Réserver ce bonheur
|
Réserver ce bonheur
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,41 @@
|
|||||||
{% extends 'revervation/base.twig' %}
|
{% extends 'revervation/base.twig' %}
|
||||||
|
|
||||||
|
{# --- SEO DYNAMIQUE & META-DONNÉES --- #}
|
||||||
{% block title %}{{ product.name }} - Location Ludikevent{% endblock %}
|
{% 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 %}
|
{% block breadcrumb_json %}
|
||||||
,{
|
,{
|
||||||
"@type": "ListItem",
|
"@type": "ListItem",
|
||||||
@@ -17,6 +51,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
{# --- TRACKING ÉVÉNEMENT --- #}
|
||||||
<utm-event event="view_product" data="{{ product.json }}"></utm-event>
|
<utm-event event="view_product" data="{{ product.json }}"></utm-event>
|
||||||
|
|
||||||
<div class="min-h-screen bg-white font-sans antialiased">
|
<div class="min-h-screen bg-white font-sans antialiased">
|
||||||
@@ -78,8 +113,8 @@
|
|||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
{# Prix principal #}
|
{# Prix principal #}
|
||||||
<div class="flex items-baseline gap-4">f
|
<div class="flex items-baseline gap-4">
|
||||||
<span class="text-6xl font-black text-blue-600 italic">{{ product.priceDay }}€</span>
|
<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>
|
<span class="text-sm font-bold text-slate-400 uppercase tracking-widest italic">La première journée</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -91,6 +126,11 @@
|
|||||||
</div>
|
</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="border-t border-slate-100 pt-10 mb-12">
|
||||||
<div class="grid grid-cols-1 gap-8">
|
<div class="grid grid-cols-1 gap-8">
|
||||||
{# Badge Âge #}
|
{# Badge Âge #}
|
||||||
@@ -116,7 +156,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# --- ACTION --- #}
|
{# --- ACTION FINALE --- #}
|
||||||
<div class="mt-auto">
|
<div class="mt-auto">
|
||||||
<a href="{{ path('reservation_contact', {id: product.id}) }}"
|
<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">
|
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">
|
||||||
@@ -132,7 +172,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{# --- SECTION SUGGESTIONS --- #}
|
{# --- SECTION SUGGESTIONS (CROSS-SELLING) --- #}
|
||||||
<section class="max-w-7xl mx-auto px-4 py-24 border-t border-slate-100 mt-12">
|
<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 class="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-16">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user