```
✨ feat(Product.php): Ajoute les entités ProductPhotos et ProductVideo. ✨ feat(Product): Ajoute les collections photos et vidéos au produit. 🆕 feat(ProductPhotosType): Crée le formulaire d'upload des photos. 🆕 feat(ProductVideoType): Crée le formulaire d'upload des vidéos. 🎨 refactor(add.twig): Ajoute les formulaires et affichage des photos/vidéos. 🎨 refactor(produit.twig): Affiche les photos et vidéos sur la page produit. ♻️ refactor(vich_uploader.yaml): Ajoute les mappings pour photos et vidéos. 🐛 fix(ProductController): Gère l'ajout/suppression des photos et vidéos. ```
This commit is contained in:
@@ -9,6 +9,14 @@ vich_uploader:
|
||||
uri_prefix: /images/image_product
|
||||
upload_destination: '%kernel.project_dir%/public/images/image_product'
|
||||
namer: Vich\UploaderBundle\Naming\SmartUniqueNamer
|
||||
image_product_pic:
|
||||
uri_prefix: /images/image_product_pic
|
||||
upload_destination: '%kernel.project_dir%/public/images/image_product_pic'
|
||||
namer: Vich\UploaderBundle\Naming\SmartUniqueNamer
|
||||
image_product_video:
|
||||
uri_prefix: /images/image_product_video
|
||||
upload_destination: '%kernel.project_dir%/public/images/image_product_video'
|
||||
namer: Vich\UploaderBundle\Naming\SmartUniqueNamer
|
||||
doc_product:
|
||||
uri_prefix: /images/doc_product
|
||||
upload_destination: '%kernel.project_dir%/public/images/doc_product'
|
||||
|
||||
42
migrations/Version20260130095656.php
Normal file
42
migrations/Version20260130095656.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?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 Version20260130095656 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('CREATE TABLE product_photos (id SERIAL NOT NULL, product_id INT DEFAULT NULL, image_name VARCHAR(255) DEFAULT NULL, image_size INT DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX IDX_6A0AA17D4584665A ON product_photos (product_id)');
|
||||
$this->addSql('COMMENT ON COLUMN product_photos.updated_at IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('CREATE TABLE product_video (id SERIAL NOT NULL, product_id INT DEFAULT NULL, image_name VARCHAR(255) DEFAULT NULL, image_size INT DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX IDX_DD9BA1704584665A ON product_video (product_id)');
|
||||
$this->addSql('COMMENT ON COLUMN product_video.updated_at IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('ALTER TABLE product_photos ADD CONSTRAINT FK_6A0AA17D4584665A FOREIGN KEY (product_id) REFERENCES product (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE product_video ADD CONSTRAINT FK_DD9BA1704584665A FOREIGN KEY (product_id) REFERENCES product (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
|
||||
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_photos DROP CONSTRAINT FK_6A0AA17D4584665A');
|
||||
$this->addSql('ALTER TABLE product_video DROP CONSTRAINT FK_DD9BA1704584665A');
|
||||
$this->addSql('DROP TABLE product_photos');
|
||||
$this->addSql('DROP TABLE product_video');
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,13 @@ namespace App\Controller\Dashboard;
|
||||
use App\Entity\Options;
|
||||
use App\Entity\Product;
|
||||
use App\Entity\ProductDoc;
|
||||
use App\Entity\ProductPhotos;
|
||||
use App\Entity\ProductVideo;
|
||||
use App\Form\OptionsType;
|
||||
use App\Form\ProductDocType;
|
||||
use App\Form\ProductPhotosType;
|
||||
use App\Form\ProductType;
|
||||
use App\Form\ProductVideoType;
|
||||
use App\Logger\AppLogger;
|
||||
use App\Repository\OptionsRepository;
|
||||
use App\Repository\ProductRepository;
|
||||
@@ -126,7 +130,7 @@ class ProductController extends AbstractController
|
||||
#[Route(path: '/crm/products/edit/{id}', name: 'app_crm_product_edit', methods: ['GET', 'POST'])]
|
||||
public function productEdit(Product $product, EntityManagerInterface $em, AppLogger $logger, Request $request, Client $stripe): Response
|
||||
{
|
||||
// 1. Gestion de la suppression de document
|
||||
// 1. Suppression de Document
|
||||
if ($idDoc = $request->query->get('idDoc')) {
|
||||
$doc = $em->getRepository(ProductDoc::class)->find($idDoc);
|
||||
if ($doc && $doc->getProduct() === $product) {
|
||||
@@ -139,7 +143,59 @@ class ProductController extends AbstractController
|
||||
return $this->redirectToRoute('app_crm_product_edit', ['id' => $product->getId()]);
|
||||
}
|
||||
|
||||
// 2. Formulaire Document
|
||||
// 2. Suppression de Vidéo
|
||||
if ($idVideo = $request->query->get('idVideo')) {
|
||||
$video = $em->getRepository(ProductVideo::class)->find($idVideo);
|
||||
if ($video && $video->getProduct() === $product) {
|
||||
$em->remove($video);
|
||||
$em->flush();
|
||||
$logger->record('DELETE', "Vidéo supprimée sur {$product->getName()}");
|
||||
$this->addFlash('success', 'Vidéo supprimée.');
|
||||
}
|
||||
return $this->redirectToRoute('app_crm_product_edit', ['id' => $product->getId()]);
|
||||
}
|
||||
|
||||
// 3. Suppression de Photo
|
||||
if ($idPhoto = $request->query->get('idPhoto')) {
|
||||
$photo = $em->getRepository(ProductPhotos::class)->find($idPhoto);
|
||||
if ($photo && $photo->getProduct() === $product) {
|
||||
$em->remove($photo);
|
||||
$em->flush();
|
||||
$logger->record('DELETE', "Photo supprimée sur {$product->getName()}");
|
||||
$this->addFlash('success', 'Photo supprimée.');
|
||||
}
|
||||
return $this->redirectToRoute('app_crm_product_edit', ['id' => $product->getId()]);
|
||||
}
|
||||
|
||||
// 4. Formulaire Ajout Photo
|
||||
$photo = new ProductPhotos();
|
||||
$photo->setProduct($product);
|
||||
$formPhotos = $this->createForm(ProductPhotosType::class, $photo);
|
||||
$formPhotos->handleRequest($request);
|
||||
|
||||
if ($formPhotos->isSubmitted() && $formPhotos->isValid()) {
|
||||
$em->persist($photo);
|
||||
$em->flush();
|
||||
$logger->record('UPDATE', "Ajout photo sur : {$product->getName()}");
|
||||
$this->addFlash('success', 'Photo ajoutée.');
|
||||
return $this->redirectToRoute('app_crm_product_edit', ['id' => $product->getId()]);
|
||||
}
|
||||
|
||||
// 5. Formulaire Ajout Vidéo
|
||||
$video = new ProductVideo();
|
||||
$video->setProduct($product);
|
||||
$formVideo = $this->createForm(ProductVideoType::class, $video);
|
||||
$formVideo->handleRequest($request);
|
||||
|
||||
if ($formVideo->isSubmitted() && $formVideo->isValid()) {
|
||||
$em->persist($video);
|
||||
$em->flush();
|
||||
$logger->record('UPDATE', "Ajout vidéo sur : {$product->getName()}");
|
||||
$this->addFlash('success', 'Vidéo ajoutée.');
|
||||
return $this->redirectToRoute('app_crm_product_edit', ['id' => $product->getId()]);
|
||||
}
|
||||
|
||||
// 6. Formulaire Ajout Document
|
||||
$doc = new ProductDoc();
|
||||
$doc->setProduct($product);
|
||||
$formDoc = $this->createForm(ProductDocType::class, $doc);
|
||||
@@ -154,7 +210,7 @@ class ProductController extends AbstractController
|
||||
return $this->redirectToRoute('app_crm_product_edit', ['id' => $product->getId()]);
|
||||
}
|
||||
|
||||
// 3. Formulaire Produit
|
||||
// 7. Formulaire Principal Produit
|
||||
$form = $this->createForm(ProductType::class, $product);
|
||||
$form->handleRequest($request);
|
||||
|
||||
@@ -171,11 +227,12 @@ class ProductController extends AbstractController
|
||||
return $this->render('dashboard/products/add.twig', [
|
||||
'form' => $form->createView(),
|
||||
'formDoc' => $formDoc->createView(),
|
||||
'formPhoto' => $formPhotos->createView(),
|
||||
'formVideo' => $formVideo->createView(),
|
||||
'product' => $product,
|
||||
'is_edit' => true
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/crm/products/delete/{id}', name: 'app_crm_product_delete', methods: ['POST'])]
|
||||
public function productDelete(Product $product, EntityManagerInterface $em, Request $request, AppLogger $logger, Client $stripe): Response
|
||||
{
|
||||
|
||||
@@ -87,11 +87,25 @@ class Product
|
||||
#[ORM\OneToMany(targetEntity: FormulesProductInclus::class, mappedBy: 'PRODUCT')]
|
||||
private Collection $formulesProductIncluses;
|
||||
|
||||
/**
|
||||
* @var Collection<int, ProductPhotos>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: ProductPhotos::class, mappedBy: 'product')]
|
||||
private Collection $productPhotos;
|
||||
|
||||
/**
|
||||
* @var Collection<int, ProductVideo>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: ProductVideo::class, mappedBy: 'product')]
|
||||
private Collection $productVideos;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->productReserves = new ArrayCollection();
|
||||
$this->productDocs = new ArrayCollection();
|
||||
$this->formulesProductIncluses = new ArrayCollection();
|
||||
$this->productPhotos = new ArrayCollection();
|
||||
$this->productVideos = new ArrayCollection();
|
||||
}
|
||||
public function slug()
|
||||
{
|
||||
@@ -400,4 +414,64 @@ class Product
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, ProductPhotos>
|
||||
*/
|
||||
public function getProductPhotos(): Collection
|
||||
{
|
||||
return $this->productPhotos;
|
||||
}
|
||||
|
||||
public function addProductPhoto(ProductPhotos $productPhoto): static
|
||||
{
|
||||
if (!$this->productPhotos->contains($productPhoto)) {
|
||||
$this->productPhotos->add($productPhoto);
|
||||
$productPhoto->setProduct($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeProductPhoto(ProductPhotos $productPhoto): static
|
||||
{
|
||||
if ($this->productPhotos->removeElement($productPhoto)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($productPhoto->getProduct() === $this) {
|
||||
$productPhoto->setProduct(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, ProductVideo>
|
||||
*/
|
||||
public function getProductVideos(): Collection
|
||||
{
|
||||
return $this->productVideos;
|
||||
}
|
||||
|
||||
public function addProductVideo(ProductVideo $productVideo): static
|
||||
{
|
||||
if (!$this->productVideos->contains($productVideo)) {
|
||||
$this->productVideos->add($productVideo);
|
||||
$productVideo->setProduct($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeProductVideo(ProductVideo $productVideo): static
|
||||
{
|
||||
if ($this->productVideos->removeElement($productVideo)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($productVideo->getProduct() === $this) {
|
||||
$productVideo->setProduct(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
114
src/Entity/ProductPhotos.php
Normal file
114
src/Entity/ProductPhotos.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\ProductPhotosRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\HttpFoundation\File\File;
|
||||
use Vich\UploaderBundle\Mapping\Attribute\Uploadable;
|
||||
use Vich\UploaderBundle\Mapping\Attribute\UploadableField;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ProductPhotosRepository::class)]
|
||||
#[Uploadable]
|
||||
class ProductPhotos
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'productPhotos')]
|
||||
private ?Product $product = null;
|
||||
|
||||
#[UploadableField(mapping: 'image_product_pic', fileNameProperty: 'imageName', size: 'imageSize')]
|
||||
private ?File $imageFile = null;
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?string $imageName = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $imageSize = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?\DateTimeImmutable $updatedAt = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getProduct(): ?Product
|
||||
{
|
||||
return $this->product;
|
||||
}
|
||||
|
||||
public function setProduct(?Product $product): static
|
||||
{
|
||||
$this->product = $product;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getImageSize(): ?int
|
||||
{
|
||||
return $this->imageSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getImageName(): ?string
|
||||
{
|
||||
return $this->imageName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return File|null
|
||||
*/
|
||||
public function getImageFile(): ?File
|
||||
{
|
||||
return $this->imageFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeImmutable|null
|
||||
*/
|
||||
public function getUpdatedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $imageSize
|
||||
*/
|
||||
public function setImageSize(?int $imageSize): void
|
||||
{
|
||||
$this->imageSize = $imageSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $imageName
|
||||
*/
|
||||
public function setImageName(?string $imageName): void
|
||||
{
|
||||
$this->imageName = $imageName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param File|null $imageFile
|
||||
*/
|
||||
public function setImageFile(?File $imageFile): void
|
||||
{
|
||||
$this->imageFile = $imageFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTimeImmutable|null $updatedAt
|
||||
*/
|
||||
public function setUpdatedAt(?\DateTimeImmutable $updatedAt): void
|
||||
{
|
||||
$this->updatedAt = $updatedAt;
|
||||
}
|
||||
}
|
||||
115
src/Entity/ProductVideo.php
Normal file
115
src/Entity/ProductVideo.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\ProductVideoRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\HttpFoundation\File\File;
|
||||
use Vich\UploaderBundle\Mapping\Attribute\Uploadable;
|
||||
use Vich\UploaderBundle\Mapping\Attribute\UploadableField;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ProductVideoRepository::class)]
|
||||
#[Uploadable]
|
||||
class ProductVideo
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'productVideos')]
|
||||
private ?Product $product = null;
|
||||
|
||||
#[UploadableField(mapping: 'image_product_video', fileNameProperty: 'imageName', size: 'imageSize')]
|
||||
private ?File $imageFile = null;
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?string $imageName = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $imageSize = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?\DateTimeImmutable $updatedAt = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getProduct(): ?Product
|
||||
{
|
||||
return $this->product;
|
||||
}
|
||||
|
||||
public function setProduct(?Product $product): static
|
||||
{
|
||||
$this->product = $product;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeImmutable|null
|
||||
*/
|
||||
public function getUpdatedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return File|null
|
||||
*/
|
||||
public function getImageFile(): ?File
|
||||
{
|
||||
return $this->imageFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getImageName(): ?string
|
||||
{
|
||||
return $this->imageName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getImageSize(): ?int
|
||||
{
|
||||
return $this->imageSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTimeImmutable|null $updatedAt
|
||||
*/
|
||||
public function setUpdatedAt(?\DateTimeImmutable $updatedAt): void
|
||||
{
|
||||
$this->updatedAt = $updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param File|null $imageFile
|
||||
*/
|
||||
public function setImageFile(?File $imageFile): void
|
||||
{
|
||||
$this->imageFile = $imageFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $imageName
|
||||
*/
|
||||
public function setImageName(?string $imageName): void
|
||||
{
|
||||
$this->imageName = $imageName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $imageSize
|
||||
*/
|
||||
public function setImageSize(?int $imageSize): void
|
||||
{
|
||||
$this->imageSize = $imageSize;
|
||||
}
|
||||
|
||||
}
|
||||
39
src/Form/ProductPhotosType.php
Normal file
39
src/Form/ProductPhotosType.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Product;
|
||||
use App\Entity\ProductDoc;
|
||||
use App\Entity\ProductPhotos;
|
||||
use App\Entity\ProductVideo;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
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;
|
||||
|
||||
class ProductPhotosType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('imageFile',FileType::class,[
|
||||
'label' => 'Videos',
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ProductPhotos::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
38
src/Form/ProductVideoType.php
Normal file
38
src/Form/ProductVideoType.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Product;
|
||||
use App\Entity\ProductDoc;
|
||||
use App\Entity\ProductVideo;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
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;
|
||||
|
||||
class ProductVideoType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('imageFile',FileType::class,[
|
||||
'label' => 'Videos',
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ProductVideo::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
43
src/Repository/ProductPhotosRepository.php
Normal file
43
src/Repository/ProductPhotosRepository.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\ProductPhotos;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<ProductPhotos>
|
||||
*/
|
||||
class ProductPhotosRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, ProductPhotos::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return ProductPhotos[] Returns an array of ProductPhotos objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('p')
|
||||
// ->andWhere('p.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('p.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?ProductPhotos
|
||||
// {
|
||||
// return $this->createQueryBuilder('p')
|
||||
// ->andWhere('p.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
43
src/Repository/ProductVideoRepository.php
Normal file
43
src/Repository/ProductVideoRepository.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\ProductVideo;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<ProductVideo>
|
||||
*/
|
||||
class ProductVideoRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, ProductVideo::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return ProductVideo[] Returns an array of ProductVideo objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('p')
|
||||
// ->andWhere('p.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('p.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?ProductVideo
|
||||
// {
|
||||
// return $this->createQueryBuilder('p')
|
||||
// ->andWhere('p.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
@@ -9,14 +9,11 @@
|
||||
<a target="_blank" rel="nofollow"
|
||||
href="https://reservation.ludikevent.fr{{ path('reservation_product_show', {id: product.slug}) }}"
|
||||
class="flex items-center px-6 py-3 bg-white/5 hover:bg-white/10 border border-white/10 hover:border-blue-500/50 text-white text-[10px] font-black uppercase tracking-[0.2em] rounded-2xl transition-all group shadow-xl backdrop-blur-md">
|
||||
|
||||
<svg class="w-4 h-4 mr-2.5 text-blue-500 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
|
||||
Voir sur le site
|
||||
|
||||
<svg class="w-3 h-3 ml-2 text-slate-300 group-hover:translate-x-1 group-hover:-translate-y-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
||||
</svg>
|
||||
@@ -24,17 +21,17 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="max-w-6xl mx-auto animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||
{{ form_start(form) }}
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
{# COLONNE GAUCHE : VISUEL & IDENTITÉ #}
|
||||
{# --- COLONNE GAUCHE --- #}
|
||||
<div class="lg:col-span-2 space-y-8">
|
||||
|
||||
{# 00. IMAGE DU PRODUIT #}
|
||||
{# SECTION IMAGE #}
|
||||
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] p-8 shadow-2xl">
|
||||
<h3 class="text-lg font-bold text-white mb-6 flex items-center">
|
||||
<span class="w-8 h-8 bg-purple-600/20 text-purple-500 rounded-lg flex items-center justify-center mr-3 text-[10px] font-black">00</span>
|
||||
@@ -42,14 +39,13 @@
|
||||
</h3>
|
||||
|
||||
<div class="flex flex-col md:flex-row items-center gap-8">
|
||||
{# Preview Image #}
|
||||
<div class="relative flex-shrink-0">
|
||||
<div class="w-48 h-48 rounded-[2rem] overflow-hidden border-2 border-white/10 bg-slate-900/50 flex items-center justify-center">
|
||||
{# L'image avec un ID spécifique pour le JS #}
|
||||
<img id="product-image-preview"
|
||||
src="{{ product.imageName ? vich_uploader_asset(product, 'imageFile') | imagine_filter('webp') : '#' }}"
|
||||
class="w-full h-full object-cover {{ product.imageName ? '' : 'hidden' }}">
|
||||
|
||||
{# L'icône de remplacement #}
|
||||
<svg id="product-image-placeholder"
|
||||
class="w-12 h-12 text-slate-700 {{ product.imageName ? 'hidden' : '' }}"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -58,10 +54,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Input File #}
|
||||
<div class="flex-1 w-full">
|
||||
{{ form_label(form.imageFile, 'Sélectionner un fichier', {'label_attr': {'class': 'text-[10px] font-black text-slate-300 uppercase tracking-widest mb-2 block'}}) }}
|
||||
|
||||
{# On enlève le "onchange" inline, on cible via l'ID généré par Symfony ou un ID fixe #}
|
||||
{{ form_widget(form.imageFile, {
|
||||
'id': 'product_image_input',
|
||||
'attr': {
|
||||
@@ -71,6 +66,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 01. INFORMATIONS GÉNÉRALES #}
|
||||
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] p-8 shadow-2xl">
|
||||
<h3 class="text-lg font-bold text-white mb-6 flex items-center">
|
||||
@@ -93,9 +89,8 @@
|
||||
{{ form_label(form.ref, 'Référence Interne (SKU)', {'label_attr': {'class': 'text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] ml-1 mb-2 block'}}) }}
|
||||
{{ 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">
|
||||
|
||||
<div class="md:col-span-2 mt-2">
|
||||
{{ form_label(form.description, 'Description détaillée', {'label_attr': {'class': 'text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] ml-1 mb-2 block'}}) }}
|
||||
{{ form_widget(form.description, {
|
||||
'attr': {
|
||||
@@ -111,8 +106,10 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 02. DIMENSIONS DU PRODUIT #}
|
||||
<div class="mt-8 backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] p-8 shadow-2xl">
|
||||
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] p-8 shadow-2xl">
|
||||
<h3 class="text-lg font-bold text-white mb-6 flex items-center">
|
||||
<span class="w-8 h-8 bg-blue-600/20 text-blue-500 rounded-lg flex items-center justify-center mr-3 text-[10px] font-black">02</span>
|
||||
Dimensions de la Structure
|
||||
@@ -155,18 +152,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# COLONNE DROITE : TARIFICATION #}
|
||||
{# --- COLONNE DROITE --- #}
|
||||
<div class="lg:col-span-1 space-y-8">
|
||||
{# 03. FINANCES & TARIFS #}
|
||||
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] p-8 shadow-2xl h-full border-l-emerald-500/10 border-l-2">
|
||||
<h3 class="text-lg font-bold text-white mb-8 flex items-center">
|
||||
<span class="w-8 h-8 bg-emerald-600/20 text-emerald-500 rounded-lg flex items-center justify-center mr-3 text-[10px] font-black">02</span>
|
||||
<span class="w-8 h-8 bg-emerald-600/20 text-emerald-500 rounded-lg flex items-center justify-center mr-3 text-[10px] font-black">03</span>
|
||||
Finances & Tarifs
|
||||
</h3>
|
||||
|
||||
{# PRIX LOCATION #}
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
{{ form_label(form.priceDay, 'Tarif 1er Jour (€) Ou Tarif weekend (si Barnums)', {'label_attr': {'class': 'text-[10px] font-black text-slate-300 uppercase tracking-widest ml-1 mb-2 block'}}) }}
|
||||
{{ form_label(form.priceDay, 'Tarif 1er Jour (€)', {'label_attr': {'class': 'text-[10px] font-black text-slate-300 uppercase tracking-widest ml-1 mb-2 block'}}) }}
|
||||
<p class="text-[9px] text-slate-500 italic mb-2 -mt-1">Ou tarif weekend pour les barnums</p>
|
||||
<div class="relative">
|
||||
{{ form_widget(form.priceDay, {'attr': {'class': 'w-full bg-slate-900/50 border-white/5 rounded-2xl text-emerald-400 font-black text-xl focus:ring-emerald-500/20 focus:border-emerald-500 transition-all py-4 px-5'}}) }}
|
||||
<span class="absolute right-5 top-1/2 -translate-y-1/2 text-slate-600 font-bold">€HT</span>
|
||||
@@ -182,9 +180,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="mt-2"/>
|
||||
{# CAUTION #}
|
||||
<div class="pt-8 border-t border-white/5">
|
||||
<hr class="mt-8 border-white/5"/>
|
||||
|
||||
<div class="pt-8">
|
||||
{{ form_label(form.caution, 'Montant de la Caution (€)', {'label_attr': {'class': 'text-[10px] font-black text-slate-300 uppercase tracking-widest ml-1 mb-2 block'}}) }}
|
||||
<div class="relative">
|
||||
{{ form_widget(form.caution, {'attr': {'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 px-5 text-center'}}) }}
|
||||
@@ -198,7 +196,7 @@
|
||||
</div>
|
||||
<p class="text-[11px] leading-relaxed text-slate-300">
|
||||
<strong class="text-white block mb-1 uppercase tracking-wider">Information importante</strong>
|
||||
Cette somme sera prélevée sur le compte bancaire du client. Conformément aux politiques de <span class="text-blue-400">Stripe</span>, les fonds peuvent être bloqués pour un maximum de <span class="font-bold text-white">4 jours</span>. Passé ce délai, aucun prélèvement ultérieur ne sera possible.
|
||||
Cette somme sera prélevée sur le compte bancaire du client. Conformément aux politiques de <span class="text-blue-400">Stripe</span>, les fonds peuvent être bloqués pour un maximum de <span class="font-bold text-white">4 jours</span>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,7 +204,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# FOOTER ACTIONS #}
|
||||
<div class="mt-12 mb-20 flex items-center justify-between backdrop-blur-xl bg-slate-900/40 p-6 rounded-[2rem] border border-white/5 shadow-xl">
|
||||
@@ -221,20 +218,20 @@
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
{# 03. DOCUMENTS TECHNIQUES (PDF) #}
|
||||
{# 04. DOCUMENTS TECHNIQUES (PDF) #}
|
||||
{% if formDoc is defined %}
|
||||
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] p-8 shadow-2xl mt-8">
|
||||
<h3 class="text-lg font-bold text-white mb-6 flex items-center">
|
||||
<span class="w-8 h-8 bg-amber-600/20 text-amber-500 rounded-lg flex items-center justify-center mr-3 text-[10px] font-black">03</span>
|
||||
<span class="w-8 h-8 bg-amber-600/20 text-amber-500 rounded-lg flex items-center justify-center mr-3 text-[10px] font-black">04</span>
|
||||
Documents & Notices
|
||||
</h3>
|
||||
|
||||
{# LISTE DES DOCUMENTS EXISTANTS #}
|
||||
{# LISTE DES DOCUMENTS #}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-10">
|
||||
{% for doc in product.productDocs %}
|
||||
<div class="flex items-center justify-between p-4 bg-white/5 border border-white/5 rounded-2xl group hover:bg-white/10 transition-all">
|
||||
<div class="flex items-center">
|
||||
<div class="w-10 h-10 bg-red-500/20 text-red-500 rounded-xl flex items-center justify-center mr-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 bg-red-500/20 text-red-500 rounded-xl flex items-center justify-center">
|
||||
<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>
|
||||
@@ -248,8 +245,8 @@
|
||||
<a href="{{ vich_uploader_asset(doc, 'docProduct') }}" target="_blank" class="p-2 text-slate-400 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="M4 16v1a2 2 0 002 2h12a2 2 0 002-2v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
||||
</a>
|
||||
<form data-turbo="false" method="post" action="{{ path('app_crm_product_edit', {'id': product.id,act:'deleted',idDoc:doc.id}) }}"
|
||||
onsubmit="return confirm('Confirmer la suppression de ce document ?');"
|
||||
<form data-turbo="false" method="post" action="{{ path('app_crm_product_edit', {'id': product.id, act:'deleted', idDoc: doc.id}) }}"
|
||||
onsubmit="return confirm('Confirmer la suppression ?');"
|
||||
class="inline-block">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ doc.id) }}">
|
||||
<button type="submit" class="p-2 text-slate-400 hover:text-rose-500 transition-colors" title="Supprimer">
|
||||
@@ -258,7 +255,6 @@
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
{# Ici tu pourrais ajouter un lien de suppression #}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
@@ -270,7 +266,7 @@
|
||||
|
||||
<div class="h-px bg-white/5 w-full mb-10"></div>
|
||||
|
||||
{# FORMULAIRE D'AJOUT #}
|
||||
{# FORMULAIRE D'AJOUT DOC #}
|
||||
{{ form_start(formDoc) }}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 items-end">
|
||||
<div>
|
||||
@@ -302,5 +298,120 @@
|
||||
{{ form_end(formDoc) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# 05. VIDEOS #}
|
||||
{% if formVideo is defined %}
|
||||
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] p-8 shadow-2xl mt-8">
|
||||
<h3 class="text-lg font-bold text-white mb-6 flex items-center">
|
||||
<span class="w-8 h-8 bg-rose-600/20 text-rose-500 rounded-lg flex items-center justify-center mr-3 text-[10px] font-black">05</span>
|
||||
Vidéos
|
||||
</h3>
|
||||
|
||||
{# LISTE DES VIDÉOS #}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-10">
|
||||
{% for video in product.productVideos %}
|
||||
<div class="relative group rounded-2xl overflow-hidden border border-white/5 bg-black/50 aspect-video shadow-lg">
|
||||
<video controls class="w-full h-full object-cover">
|
||||
<source src="{{ vich_uploader_asset(video, 'imageFile') }}" type="video/mp4">
|
||||
Votre navigateur ne supporte pas la vidéo.
|
||||
</video>
|
||||
{# DELETE BUTTON VIDEO #}
|
||||
<div class="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-all z-20">
|
||||
<form data-turbo="false" method="post" action="{{ path('app_crm_product_edit', {'id': product.id, act:'deleteVideo', idVideo: video.id}) }}"
|
||||
onsubmit="return confirm('Supprimer cette vidéo ?');" class="inline-block">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ video.id) }}">
|
||||
<button type="submit" class="p-2.5 bg-black/60 hover:bg-rose-600 text-white rounded-xl backdrop-blur-md transition-all shadow-lg hover:scale-105">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="md:col-span-2 py-8 text-center border-2 border-dashed border-white/5 rounded-3xl">
|
||||
<p class="text-[10px] font-black text-slate-600 uppercase tracking-[0.2em]">Aucune vidéo</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="h-px bg-white/5 w-full mb-10"></div>
|
||||
|
||||
{# FORMULAIRE D'AJOUT VIDEO #}
|
||||
{{ form_start(formVideo) }}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 items-end">
|
||||
<div class="md:col-span-2 bg-slate-950/40 p-6 rounded-[2rem] border border-dashed border-white/10 group hover:border-rose-500/30 transition-all">
|
||||
{{ form_label(formVideo.imageFile, 'Fichier Vidéo (MP4, WebM...)', {'label_attr': {'class': 'text-[10px] font-black text-slate-300 uppercase tracking-widest mb-4 block text-center'}}) }}
|
||||
{{ form_widget(formVideo.imageFile, {
|
||||
'attr': {
|
||||
'class': 'block w-full text-xs text-slate-400 file:mr-4 file:py-3 file:px-6 file:rounded-xl file:border-0 file:text-[10px] file:font-black file:uppercase file:tracking-widest file:bg-rose-600 file:text-white hover:file:bg-rose-500 transition-all cursor-pointer'
|
||||
}
|
||||
}) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex justify-end">
|
||||
<button type="submit" class="group px-8 py-4 bg-rose-600/10 hover:bg-rose-600 text-rose-500 hover:text-white text-[10px] font-black uppercase tracking-widest rounded-2xl transition-all border border-rose-500/20 flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 4v16m8-8H4"/></svg>
|
||||
Ajouter la vidéo
|
||||
</button>
|
||||
</div>
|
||||
{{ form_end(formVideo) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# 06. PHOTOS #}
|
||||
{% if formPhoto is defined %}
|
||||
<div class="backdrop-blur-xl bg-[#1e293b]/40 border border-white/5 rounded-[2.5rem] p-8 shadow-2xl mt-8">
|
||||
<h3 class="text-lg font-bold text-white mb-6 flex items-center">
|
||||
<span class="w-8 h-8 bg-cyan-600/20 text-cyan-500 rounded-lg flex items-center justify-center mr-3 text-[10px] font-black">06</span>
|
||||
Photos Supplémentaires
|
||||
</h3>
|
||||
|
||||
{# LISTE DES PHOTOS #}
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 mb-10">
|
||||
{% for photo in product.productPhotos %}
|
||||
<div class="relative group rounded-2xl overflow-hidden border border-white/5 aspect-square shadow-lg">
|
||||
<img src="{{ vich_uploader_asset(photo, 'imageFile') }}" class="w-full h-full object-cover transform group-hover:scale-110 transition-transform duration-700">
|
||||
{# DELETE BUTTON PHOTO #}
|
||||
<div class="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-all z-20">
|
||||
<form data-turbo="false" method="post" action="{{ path('app_crm_product_edit', {'id': product.id, act:'deletePhoto', idPhoto: photo.id}) }}"
|
||||
onsubmit="return confirm('Supprimer cette photo ?');" class="inline-block">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ photo.id) }}">
|
||||
<button type="submit" class="p-2.5 bg-black/60 hover:bg-rose-600 text-white rounded-xl backdrop-blur-md transition-all shadow-lg hover:scale-105">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-span-full py-8 text-center border-2 border-dashed border-white/5 rounded-3xl">
|
||||
<p class="text-[10px] font-black text-slate-600 uppercase tracking-[0.2em]">Aucune photo</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="h-px bg-white/5 w-full mb-10"></div>
|
||||
|
||||
{# FORMULAIRE D'AJOUT PHOTO #}
|
||||
{{ form_start(formPhoto) }}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 items-end">
|
||||
<div class="md:col-span-2 bg-slate-950/40 p-6 rounded-[2rem] border border-dashed border-white/10 group hover:border-cyan-500/30 transition-all">
|
||||
{{ form_label(formPhoto.imageFile, 'Fichier Image (JPG, PNG...)', {'label_attr': {'class': 'text-[10px] font-black text-slate-300 uppercase tracking-widest mb-4 block text-center'}}) }}
|
||||
{{ form_widget(formPhoto.imageFile, {
|
||||
'attr': {
|
||||
'class': 'block w-full text-xs text-slate-400 file:mr-4 file:py-3 file:px-6 file:rounded-xl file:border-0 file:text-[10px] file:font-black file:uppercase file:tracking-widest file:bg-cyan-600 file:text-white hover:file:bg-cyan-500 transition-all cursor-pointer'
|
||||
}
|
||||
}) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex justify-end">
|
||||
<button type="submit" class="group px-8 py-4 bg-cyan-600/10 hover:bg-cyan-600 text-cyan-500 hover:text-white text-[10px] font-black uppercase tracking-widest rounded-2xl transition-all border border-cyan-500/20 flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 4v16m8-8H4"/></svg>
|
||||
Ajouter la photo
|
||||
</button>
|
||||
</div>
|
||||
{{ form_end(formPhoto) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -242,6 +242,40 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- GALERIE PHOTOS --- #}
|
||||
{% if product.productPhotos|length > 0 %}
|
||||
<div class="mt-20">
|
||||
<span class="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] mb-8 block italic text-center md:text-left">Galerie Photos</span>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6">
|
||||
{% for photo in product.productPhotos %}
|
||||
<div class="relative overflow-hidden rounded-[2rem] bg-slate-50 aspect-square shadow-sm hover:shadow-xl transition-all duration-500 group">
|
||||
<img src="{{ vich_uploader_asset(photo, 'imageFile') }}"
|
||||
alt="Photo {{ product.name }}"
|
||||
class="w-full h-full object-cover transform group-hover:scale-110 transition-transform duration-700"
|
||||
loading="lazy">
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# --- VIDEOS --- #}
|
||||
{% if product.productVideos|length > 0 %}
|
||||
<div class="mt-20">
|
||||
<span class="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] mb-8 block italic text-center md:text-left">Vidéos de présentation</span>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{% for video in product.productVideos %}
|
||||
<div class="relative overflow-hidden rounded-[2.5rem] bg-black shadow-lg aspect-video group">
|
||||
<video controls class="w-full h-full object-cover" preload="metadata">
|
||||
<source src="{{ vich_uploader_asset(video, 'imageFile') }}" type="video/mp4">
|
||||
Votre navigateur ne supporte pas la lecture de vidéos.
|
||||
</video>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# --- DOCUMENTS PUBLICS --- #}
|
||||
{% set publicDocs = product.productDocs|filter(doc => doc.isPublic) %}
|
||||
{% if publicDocs|length > 0 %}
|
||||
|
||||
Reference in New Issue
Block a user