feat(Product): Ajoute description et quantité aux produits, et formulaire associé.
```
This commit is contained in:
Serreau Jovann
2026-01-21 14:38:16 +01:00
parent e3c42a7aa4
commit 2afd6e6be8
8 changed files with 170 additions and 7 deletions

View 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');
}
}

View 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');
}
}

View File

@@ -6,6 +6,7 @@ use App\Repository\ProductRepository;
use Cocur\Slugify\Slugify;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Attribute\Uploadable;
@@ -68,6 +69,12 @@ class Product
#[ORM\OneToMany(targetEntity: ProductReserve::class, mappedBy: 'product')]
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()
{
$this->devisLines = new ArrayCollection();
@@ -285,4 +292,28 @@ class Product
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;
}
}

View File

@@ -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\NumberType;
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\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -27,9 +28,21 @@ class ProductType extends AbstractType
'label' => 'Reference du produit',
'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',
'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,[
'label' => 'Caution du produit',

View File

@@ -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'}}) }}
</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>

View File

@@ -40,8 +40,8 @@
</button>
{% 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-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': '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'},

View File

@@ -155,7 +155,7 @@
</div>
<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
</a>
</div>

View File

@@ -1,7 +1,41 @@
{% 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",
@@ -17,6 +51,7 @@
{% 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">
@@ -78,8 +113,8 @@
<div class="space-y-6">
{# Prix principal #}
<div class="flex items-baseline gap-4">f
<span class="text-6xl font-black text-blue-600 italic">{{ product.priceDay }}€</span>
<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>
@@ -91,6 +126,11 @@
</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 #}
@@ -116,7 +156,7 @@
</div>
</div>
{# --- ACTION --- #}
{# --- 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">
@@ -132,7 +172,7 @@
</div>
</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">
<div class="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-16">
<div>