Browse Source

jeux, utilisateur et genres avec prise en charge JS par stimulus

garthh 2 days ago
parent
commit
052963acff

+ 3 - 1
.env

@@ -45,4 +45,6 @@ MAILER_DSN=null://null
 APP_URL=http://localhost:8000
 APP_TZ="Europe/Paris"
 CONTACT_EMAIL=no-reply@mail.com
-CONTACT_NAME=Orgasso
+CONTACT_NAME=Orgasso
+APP_ALLOW_REGISTER=true
+APP_ALLOW_PUBLIC_EVENT=true

+ 29 - 0
assets/controllers/admin_game_controller.js

@@ -0,0 +1,29 @@
+import { Controller } from '@hotwired/stimulus';
+
+/*
+ * Contrôleur Stimulus pour le formulaire JEUX.
+ */
+
+export default class extends Controller {
+
+    connect() {
+        this.initDisabling();
+        console.log("Stimulus: gestion des champs à désactiver");
+        
+    }
+
+    initDisabling() {
+        const checkInLibrary = document.querySelector('#library-controller input');
+        checkInLibrary.addEventListener('change', () => {
+            const formToDisable = Array.from(document.querySelectorAll(".only-in-library"));
+            formToDisable.forEach((el) => {
+                if (checkInLibrary.checked) {
+                    el.classList.remove("is-hidden");
+                } else {
+                    el.classList.add("is-hidden");   
+                }
+            });
+        });
+    }
+
+}

+ 23 - 0
assets/controllers/bulma_filenames_controller.js

@@ -0,0 +1,23 @@
+import { Controller } from '@hotwired/stimulus';
+
+/*
+ * Contrôleur Stimulus pour les boutons d'uploads.
+ */
+
+export default class extends Controller {
+
+    connect() {
+        this.initFilenameLink();
+        console.log("Stimulus: gestion nom du ficher en upload");
+    }
+
+    initFilenameLink() {
+        const fileInput = document.querySelector('#file-js input[type=file]');
+
+        fileInput.addEventListener('change', () => {
+            const fileName = document.querySelector("#file-js .file-name");
+            console.log(fileInput.files);
+            fileName.textContent = fileInput.files[0].name;
+        });
+    }
+}

+ 6 - 1
assets/styles/app.css

@@ -7,4 +7,9 @@
 
 .icon-inner {
     padding: 0.5rem;
-} 
+} 
+
+.small-icon-in-text {
+    height: 1rem;
+    
+}

+ 1 - 0
config/services.yaml

@@ -4,6 +4,7 @@
 # Put parameters here that don't need to change on each machine where the app is deployed
 # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
 parameters:
+    upload_images_directory: '%kernel.project_dir%/public/images'
 
 services:
     # default configuration for services in *this* file

+ 31 - 0
migrations/Version20250725192841.php

@@ -0,0 +1,31 @@
+<?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 Version20250725192841 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 game (id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL, description LONGTEXT DEFAULT NULL, is_physical TINYINT(1) DEFAULT NULL, is_numerical TINYINT(1) DEFAULT NULL, url_numerical_version VARCHAR(255) DEFAULT NULL, is_in_asso_library TINYINT(1) DEFAULT NULL, is_borrowed TINYINT(1) DEFAULT NULL, picture VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('DROP TABLE game');
+    }
+}

+ 35 - 0
migrations/Version20250725194830.php

@@ -0,0 +1,35 @@
+<?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 Version20250725194830 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 game_genre (game_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', genre_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', INDEX IDX_B1634A77E48FD905 (game_id), INDEX IDX_B1634A774296D31F (genre_id), PRIMARY KEY(game_id, genre_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+        $this->addSql('ALTER TABLE game_genre ADD CONSTRAINT FK_B1634A77E48FD905 FOREIGN KEY (game_id) REFERENCES game (id) ON DELETE CASCADE');
+        $this->addSql('ALTER TABLE game_genre ADD CONSTRAINT FK_B1634A774296D31F FOREIGN KEY (genre_id) REFERENCES genre (id) ON DELETE CASCADE');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE game_genre DROP FOREIGN KEY FK_B1634A77E48FD905');
+        $this->addSql('ALTER TABLE game_genre DROP FOREIGN KEY FK_B1634A774296D31F');
+        $this->addSql('DROP TABLE game_genre');
+    }
+}

+ 35 - 0
migrations/Version20250728151401.php

@@ -0,0 +1,35 @@
+<?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 Version20250728151401 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 game ADD add_by_id BINARY(16) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', ADD add_datetime DATETIME DEFAULT NULL');
+        $this->addSql('ALTER TABLE game ADD CONSTRAINT FK_232B318C17542AC5 FOREIGN KEY (add_by_id) REFERENCES user (id)');
+        $this->addSql('CREATE INDEX IDX_232B318C17542AC5 ON game (add_by_id)');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE game DROP FOREIGN KEY FK_232B318C17542AC5');
+        $this->addSql('DROP INDEX IDX_232B318C17542AC5 ON game');
+        $this->addSql('ALTER TABLE game DROP add_by_id, DROP add_datetime');
+    }
+}

+ 31 - 0
migrations/Version20250728151622.php

@@ -0,0 +1,31 @@
+<?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 Version20250728151622 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 game ADD is_valid_by_admin TINYINT(1) NOT NULL');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE game DROP is_valid_by_admin');
+    }
+}

+ 31 - 0
migrations/Version20250728165702.php

@@ -0,0 +1,31 @@
+<?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 Version20250728165702 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 game ADD valid_datetime DATETIME DEFAULT NULL, CHANGE is_valid_by_admin is_valid_by_admin TINYINT(1) DEFAULT NULL');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE game DROP valid_datetime, CHANGE is_valid_by_admin is_valid_by_admin TINYINT(1) NOT NULL');
+    }
+}

BIN
public/images/games/bgs.webp


+ 152 - 0
src/Controller/Admin/GameController.php

@@ -0,0 +1,152 @@
+<?php
+
+namespace App\Controller\Admin;
+
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Requirement\Requirement;
+use Symfony\Component\Routing\Attribute\Route;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Component\String\Slugger\AsciiSlugger;
+use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
+
+use App\Entity\Game;
+use App\Form\GameType;
+use App\Repository\GameRepository;
+use App\Service\PictureService;
+
+final class GameController extends AbstractController
+{
+    /*
+     * Lister tous les jeux
+     */
+    #[Route('/admin/game', name: 'app_admin_game', methods: ['GET'])]
+    public function index(GameRepository $repository): Response
+    {
+        $games = $repository->findAll();
+
+        return $this->render('admin/game/index.html.twig', [
+            'games' => $games,
+        ]);
+    }
+
+    /*
+     * Supprimer un jeu
+     */
+    #[Route('/admin/game/{id}/delete', name: 'app_admin_game_delete', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
+    public function delete(?Game $game, EntityManagerInterface $manager, ParameterBagInterface $params): Response
+    {
+        // Effacer toutes images associée
+        $fullPath = $params->get('upload_images_directory') . '/games/' . $game->getPicture();
+        if (file_exists($fullPath)) {
+            unlink($fullPath);
+        }
+        
+        $manager->remove($game);
+        $manager->flush();
+        
+        $this->addFlash('success', 'Jeu supprimé avec succès.');
+        return $this->redirectToRoute('app_admin_game');
+    }
+
+    /*
+     * Supprimer l'image un jeu
+     */
+    #[Route('/admin/game/{id}/del-pic', name: 'app_admin_game_del_pic', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
+    public function deletePicture(?Game $game, EntityManagerInterface $manager, ParameterBagInterface $params): Response
+    {
+        // Effacer toutes images associée
+        $fullPath = $params->get('upload_images_directory') . '/games/' . $game->getPicture();
+        if (file_exists($fullPath)) {
+            unlink($fullPath);
+        }
+        $game->setPicture(null);
+        $manager->flush();
+        
+        $this->addFlash('success', 'Illustration supprimée avec succès.');
+        return $this->redirectToRoute('app_admin_game_edit', ['id' => $game->getId()]);
+    }
+
+    /* 
+     * Modifier un jeu
+     */
+    #[Route('/admin/game/{id}/edit', name: 'app_admin_game_edit', requirements: ['id' => Requirement::UUID_V7], methods: ['GET', 'POST'])]
+    public function edit(?Game $game, Request $request, EntityManagerInterface $manager, PictureService $pictureService): Response
+    {
+        $form = $this->createForm(GameType::class, $game);
+
+        $form->handleRequest($request);
+        if ($form->isSubmitted() && $form->isValid()) {
+            $slug = $form->get('slug')->getData();
+            // Aucun slug n'a été proposé par l'utilisateur
+            if (!$slug) {
+                $slugger = new AsciiSlugger('fr_FR');
+                $slug = $slugger->slug(strtolower($game->getName()));
+                $game->setSlug($slug);
+            }
+            
+            // Traiter l'image proposée
+            $tmpPicture = $form->get('picture')->getData();
+            if ($tmpPicture) {
+                $picture = $pictureService->banner($tmpPicture, '/games/', $slug);
+                $game->setPicture($picture);
+            }
+            $manager->persist($game);
+            $manager->flush();
+
+            $this->addFlash('success', 'Jeu modifié avec succès.');
+            return $this->redirectToRoute('app_admin_game');
+        }
+
+        return $this->render('admin/game/edit.html.twig', [
+            'form' => $form,
+            'game' => $game,
+        ]);
+    }
+
+    /* 
+     * Ajouter un jeu
+     */
+    #[Route('/admin/game/add', name: 'app_admin_game_add', methods: ['GET', 'POST'])]
+    public function add(Request $request, EntityManagerInterface $manager, PictureService $pictureService): Response
+    {
+        // Initialisation d'un nouveau jeu
+        $game = new Game();
+        // Initialisaiton des valeurs par défaut
+        $game->setAddBy($this->getUser());
+        $game->setAddDatetime(new \Datetime('now'));
+
+        $form = $this->createForm(GameType::class, $game);
+
+        $form->handleRequest($request);
+        if ($form->isSubmitted() && $form->isValid()) {
+            
+            $slug = $form->get('slug')->getData();
+            // Aucun slug n'a été proposé par l'utilisateur
+            if (!$slug) {
+                $slugger = new AsciiSlugger('fr_FR');
+                $slug = $slugger->slug(strtolower($game->getName()));
+                $game->setSlug($slug);
+            }
+            
+            // Traiter l'image proposée
+            $tmpPicture = $form->get('picture')->getData();
+            if ($tmpPicture) {
+                $picture = $pictureService->banner($tmpPicture, '/games/', $slug);
+                $game->setPicture($picture);
+            }
+
+            $manager->persist($game);
+            $manager->flush();
+
+            $this->addFlash('success', 'Jeu ajouté à la base de données avec succès.');
+            return $this->redirectToRoute('app_admin_game');
+        }
+
+        return $this->render('admin/game/edit.html.twig', [
+            'form' => $form,
+            'game' => $game
+        ]);
+    }
+}

+ 2 - 2
src/Controller/Admin/GenreController.php

@@ -54,7 +54,7 @@ final class GenreController extends AbstractController
             // Aucun slug n'a été proposé par l'utilisateur
             if (!$form->get('slug')->getData()) {
                 $slugger = new AsciiSlugger('fr_FR');
-                $genre->setSlug($slugger->slug($genre->getGenre()));
+                $genre->setSlug($slugger->slug(strtolower($genre->getGenre())));
             }
             $manager->persist($genre);
             $manager->flush();
@@ -83,7 +83,7 @@ final class GenreController extends AbstractController
             // Aucun slug n'a été proposé par l'utilisateur
             if (!$form->get('slug')->getData()) {
                 $slugger = new AsciiSlugger('fr_FR');
-                $genre->setSlug($slugger->slug($genre->getGenre()));
+                $genre->setSlug($slugger->slug(strtolower($genre->getGenre())));
             }
             $manager->persist($genre);
             $manager->flush();

+ 265 - 0
src/Entity/Game.php

@@ -0,0 +1,265 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\GameRepository;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\ORM\Mapping as ORM;
+use Doctrine\DBAL\Types\Types;
+use Symfony\Bridge\Doctrine\Types\UuidType;
+use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
+use Symfony\Component\Uid\Uuid;
+
+#[ORM\Entity(repositoryClass: GameRepository::class)]
+class Game
+{
+    #[ORM\Id]
+    #[ORM\Column(type: UuidType::NAME, unique: true)]
+    #[ORM\GeneratedValue(strategy: 'CUSTOM')]
+    #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
+    private ?Uuid $id = null;
+
+    #[ORM\Column(length: 255)]
+    private ?string $name = null;
+
+    #[ORM\Column(length: 255)]
+    private ?string $slug = null;
+
+    #[ORM\Column(type: Types::TEXT, nullable: true)]
+    private ?string $description = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?bool $isPhysical = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?bool $isNumerical = null;
+
+    #[ORM\Column(length: 255, nullable: true)]
+    private ?string $urlNumericalVersion = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?bool $isInAssoLibrary = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?bool $isBorrowed = null;
+
+    #[ORM\Column(length: 255, nullable: true)]
+    private ?string $picture = null;
+
+    /**
+     * @var Collection<int, Genre>
+     */
+    #[ORM\ManyToMany(targetEntity: Genre::class, inversedBy: 'games')]
+    private Collection $genre;
+
+    #[ORM\ManyToOne(inversedBy: 'games')]
+    private ?User $addBy = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?\DateTime $addDatetime = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?bool $isValidByAdmin = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?\DateTime $validDatetime = null;
+
+    public function __construct()
+    {
+        $this->genre = new ArrayCollection();
+        $this->id = Uuid::v7();
+    }
+
+    public function getId(): ?Uuid
+    {
+        return $this->id;
+    }
+
+    public function setId(Uuid $id): static
+    {
+        $this->id = $id;
+
+        return $this;
+    }
+
+    public function getName(): ?string
+    {
+        return $this->name;
+    }
+
+    public function setName(string $name): static
+    {
+        $this->name = $name;
+
+        return $this;
+    }
+
+    public function getSlug(): ?string
+    {
+        return $this->slug;
+    }
+
+    public function setSlug(string $slug): static
+    {
+        $this->slug = $slug;
+
+        return $this;
+    }
+
+    public function getDescription(): ?string
+    {
+        return $this->description;
+    }
+
+    public function setDescription(?string $description): static
+    {
+        $this->description = $description;
+
+        return $this;
+    }
+
+    public function isPhysical(): ?bool
+    {
+        return $this->isPhysical;
+    }
+
+    public function setIsPhysical(?bool $isPhysical): static
+    {
+        $this->isPhysical = $isPhysical;
+
+        return $this;
+    }
+
+    public function isNumerical(): ?bool
+    {
+        return $this->isNumerical;
+    }
+
+    public function setIsNumerical(bool $isNumerical): static
+    {
+        $this->isNumerical = $isNumerical;
+
+        return $this;
+    }
+
+    public function getUrlNumericalVersion(): ?string
+    {
+        return $this->urlNumericalVersion;
+    }
+
+    public function setUrlNumericalVersion(?string $urlNumericalVersion): static
+    {
+        $this->urlNumericalVersion = $urlNumericalVersion;
+
+        return $this;
+    }
+
+    public function isInAssoLibrary(): ?bool
+    {
+        return $this->isInAssoLibrary;
+    }
+
+    public function setIsInAssoLibrary(?bool $isInAssoLibrary): static
+    {
+        $this->isInAssoLibrary = $isInAssoLibrary;
+
+        return $this;
+    }
+
+    public function isBorrowed(): ?bool
+    {
+        return $this->isBorrowed;
+    }
+
+    public function setIsBorrowed(?bool $isBorrowed): static
+    {
+        $this->isBorrowed = $isBorrowed;
+
+        return $this;
+    }
+
+    public function getPicture(): ?string
+    {
+        return $this->picture;
+    }
+
+    public function setPicture(?string $picture): static
+    {
+        $this->picture = $picture;
+
+        return $this;
+    }
+
+    /**
+     * @return Collection<int, Genre>
+     */
+    public function getGenre(): Collection
+    {
+        return $this->genre;
+    }
+
+    public function addGenre(Genre $genre): static
+    {
+        if (!$this->genre->contains($genre)) {
+            $this->genre->add($genre);
+        }
+
+        return $this;
+    }
+
+    public function removeGenre(Genre $genre): static
+    {
+        $this->genre->removeElement($genre);
+
+        return $this;
+    }
+
+    public function getAddBy(): ?User
+    {
+        return $this->addBy;
+    }
+
+    public function setAddBy(?User $addBy): static
+    {
+        $this->addBy = $addBy;
+
+        return $this;
+    }
+
+    public function getAddDatetime(): ?\DateTime
+    {
+        return $this->addDatetime;
+    }
+
+    public function setAddDatetime(?\DateTime $addDatetime): static
+    {
+        $this->addDatetime = $addDatetime;
+
+        return $this;
+    }
+
+    public function isValidByAdmin(): ?bool
+    {
+        return $this->isValidByAdmin;
+    }
+
+    public function setIsValidByAdmin(bool $isValidByAdmin): static
+    {
+        $this->isValidByAdmin = $isValidByAdmin;
+
+        return $this;
+    }
+
+    public function getValidDatetime(): ?\DateTime
+    {
+        return $this->validDatetime;
+    }
+
+    public function setValidDatetime(?\DateTime $validDatetime): static
+    {
+        $this->validDatetime = $validDatetime;
+
+        return $this;
+    }
+}

+ 40 - 0
src/Entity/Genre.php

@@ -3,6 +3,8 @@
 namespace App\Entity;
 
 use App\Repository\GenreRepository;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
 use Doctrine\DBAL\Types\Types;
 use Symfony\Bridge\Doctrine\Types\UuidType;
@@ -24,6 +26,12 @@ class Genre
     #[ORM\Column(length: 255)]
     private ?string $slug = null;
 
+    /**
+     * @var Collection<int, Game>
+     */
+    #[ORM\ManyToMany(targetEntity: Game::class, mappedBy: 'genre')]
+    private Collection $games;
+
     public function getId(): ?Uuid
     {
         return $this->id;
@@ -59,4 +67,36 @@ class Genre
 
         return $this;
     }
+
+    /**
+     * @return Collection<int, Game>
+     */
+    public function getGames(): Collection
+    {
+        return $this->games;
+    }
+
+    public function getHowManygames(): int 
+    {
+        return count($this->games);
+    }
+
+    public function addGame(Game $game): static
+    {
+        if (!$this->games->contains($game)) {
+            $this->games->add($game);
+            $game->addGenre($this);
+        }
+
+        return $this;
+    }
+
+    public function removeGame(Game $game): static
+    {
+        if ($this->games->removeElement($game)) {
+            $game->removeGenre($this);
+        }
+
+        return $this;
+    }
 }

+ 45 - 0
src/Entity/User.php

@@ -3,6 +3,8 @@
 namespace App\Entity;
 
 use App\Repository\UserRepository;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
 use Doctrine\DBAL\Types\Types;
 use Symfony\Bridge\Doctrine\Types\UuidType;
@@ -62,9 +64,16 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
     #[ORM\Column(type: Types::DATETIME_MUTABLE,nullable: true)]
     private ?\DateTimeInterface $lastAction = null;
 
+    /**
+     * @var Collection<int, Game>
+     */
+    #[ORM\OneToMany(targetEntity: Game::class, mappedBy: 'addBy')]
+    private Collection $games;
+
     public function __construct()
     {
         $this->lastUpdate = new \DateTime('now');
+        $this->games = new ArrayCollection();
     }
 
     public function getId(): ?Uuid
@@ -171,6 +180,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
         return $this;
     }
 
+    public function getFullName(): ?string
+    {
+        $fullName = $this->firstName . " " . $this->lastName; 
+        return $fullName;
+    }
+
     /**
      * Getter / Setter pour numéro de téléphone
      */
@@ -270,4 +285,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
 
         return $this;
     }
+
+    /**
+     * @return Collection<int, Game>
+     */
+    public function getGames(): Collection
+    {
+        return $this->games;
+    }
+
+    public function addGame(Game $game): static
+    {
+        if (!$this->games->contains($game)) {
+            $this->games->add($game);
+            $game->setAddBy($this);
+        }
+
+        return $this;
+    }
+
+    public function removeGame(Game $game): static
+    {
+        if ($this->games->removeElement($game)) {
+            // set the owning side to null (unless already changed)
+            if ($game->getAddBy() === $this) {
+                $game->setAddBy(null);
+            }
+        }
+
+        return $this;
+    }
 }

+ 149 - 0
src/Form/GameType.php

@@ -0,0 +1,149 @@
+<?php
+
+namespace App\Form;
+
+use App\Entity\Game;
+use App\Entity\Genre;
+use App\Entity\User;
+use Symfony\Bridge\Doctrine\Form\Type\EntityType;
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
+use Symfony\Component\Form\Extension\Core\Type\FileType;
+use Symfony\Component\Form\Extension\Core\Type\UrlType;
+
+class GameType extends AbstractType
+{
+    public function buildForm(FormBuilderInterface $builder, array $options): void
+    {
+        $builder
+            ->add('name', null, [
+                'label' => 'Nom du jeu',
+                'empty_data' => 'Donjons & Dragons',
+                'required' => true,
+                'help' => 'Entrez le nom du jeu.',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('slug', null, [
+                'label' => 'Slug',
+                'empty_data' => 'donjons-et-dragons',
+                'help' => 'Laissez vide pour le générer automatiquement, utilisé dans les URL.',
+                'required' => false,
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('description', null, [
+                'label' => 'Description',
+                'label_attr' => ['class' => 'label'],
+                'help' => 'Description sommaire du jeu.',
+                'attr' => ['class' => 'textarea',
+                           'rows' => 6],
+                'required' => false,
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+
+            ])
+            ->add('picture', FileType::class, [
+                'label' => 'Illustration',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'file-input'],
+                'help' => 'Fichier JPEG, PNG ou webP, bannière horizontale, sera découpée et redimensionnée en 600×200',
+                'required' => false,
+                'mapped' => false,
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('genre', EntityType::class, [
+                'label' => 'Genres',
+                'attr' => ['class' => 'checkboxes'],
+                'class' => Genre::class,
+                'choice_label' => 'genre',
+                'expanded' => true,
+                'multiple' => true,
+                'label_attr' => ['class' => 'label'],
+                'choice_attr' => ['class' => 'checkbox'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('isInAssoLibrary', null, [
+                'label' => 'Dans la ludothèque de l\'association ?',
+                'label_attr' => ['class' => 'checkbox'],
+                'help' => 'Cochez si ce jeu est disponible dans la ludothèque de l\'association.',
+                'attr' => ['class' => 'checkbox'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('isPhysical', null, [
+                'label' => 'Version papier ?',
+                'label_attr' => ['class' => 'checkbox'],
+                'help' => 'Cochez si ce jeu est disponible en version physique dans la ludothèque de l\'association.',
+                'attr' => ['class' => 'checkbox'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('isNumerical', null, [
+                'label' => 'Version numérique ?',
+                'label_attr' => ['class' => 'checkbox'],
+                'help' => 'Cochez si ce jeu est disponible en version numérique (PDF, epub...) dans la ludothèque de l\'association.',
+                'attr' => ['class' => 'checkbox'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('urlNumericalVersion', UrlType::class, [
+                'label' => 'URL de la version numérique',
+                'help' => 'Lien privé pour accéder à la version numérique (PDF, epub...).',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'required' => false,
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('addBy', EntityType::class, [
+                'label' => 'Créateur de la fiche du jeu',
+                'class' => User::class,
+                'choice_label' => 'fullName',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'required' => true,
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],               
+            ])
+            ->add('addDatetime', null, [
+                'label' => 'Date de création de la fiche',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'required' => true,
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('isValidByAdmin', null, [
+                'label' => 'Fiche validée par un administrateur ?',
+                'label_attr' => ['class' => 'checkbox'],
+                'attr' => ['class' => 'checkbox'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+                'help' => 'Les jeux qui ne sont pas validés ne peuvent pas être utilisés pour créer une partie.'
+            ])
+            ->add('validDatetime', null, [
+                'label' => 'Date de validation de la fiche',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'required' => false,
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ]);
+
+    }
+
+    public function configureOptions(OptionsResolver $resolver): void
+    {
+        $resolver->setDefaults([
+            'data_class' => Game::class,
+        ]);
+    }
+}

+ 10 - 0
src/Form/GenreType.php

@@ -14,10 +14,20 @@ class GenreType extends AbstractType
         $builder
             ->add('genre', null, [
                 'label' => 'Genre',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+
                 'required' => true,
                 ])
             ->add('slug', null, [
                 'label' => 'Slug',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+
                 'help' => 'Laissez vide pour le générer automatiquement',
                 'required' => false,
             ])

+ 44 - 10
src/Form/UserType.php

@@ -18,39 +18,67 @@ class UserType extends AbstractType
         $builder
             ->add('firstName', null, [
                 'label' => 'Prénom',
+                'empty_data' => 'Anne',
                 'required' => true,
-                'help' => 'Entrez le prénom de l\'utilisateur.'
+                'help' => 'Entrez le prénom de l\'utilisateur.',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
             ])
             ->add('lastName', null, [
                 'label' => 'Nom',
+                'empty_data' => 'Nonyme',
                 'required' => true,
-                'help' => 'Entrez le nom de l\'utilisateur.'
+                'help' => 'Entrez le nom de l\'utilisateur.',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
             ])
             ->add('email', null, [
-                'label' => 'eMail',
+                'label' => 'email',
+                'empty_data' => 'a.nonyme@courriel.fr',
                 'required' => true,
-                'help' => 'Entrez une adresse email.'
+                'help' => 'Entrez une adresse email.',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
             ])
             ->add('isVerified', null, [
-                'label' => 'email vérifié',
+                'label' => 'L\'adresse email de l\'utilisateur a été vérifiée ?',
                 'help' => 'Cochez si l\'adresse email est vérifiée. La connexion est impossible si l\'email n\'est pas vérifié.',
+                'label_attr' => ['class' => 'checkbox'],
+                'attr' => ['class' => 'checkbox'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
             ])
             ->add('phone', null, [
-                'label' => 'Téléphone',
+                'label' => 'Numéro de téléphone mobile',
+                'empty_data' => '06 00 00 00 00',
                 'required' => false,
-                'help' => 'Entrez un numéro de téléphone (optionnel).'
+                'help' => 'Entrez un numéro de téléphone mobile (optionnel).',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
             ])
             ->add('roles', ChoiceType::class, [
                 'label' => 'Rôles',
                 'help' => 'Sélectionnez les rôles de l\'utilisateur.',
                 'choices' => [
                     'Participant' => 'ROLE_USER',
-                    'Equipe' => 'ROLE_STAFF',
+                    'Équipe' => 'ROLE_STAFF',
                     'Gestionnaire' => 'ROLE_MANAGER',
                     'Administrateur' => 'ROLE_ADMIN',
                 ],
                 'expanded' => true,
                 'multiple' => true,
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'checkboxes'],
+                'choice_attr' => ['class' => 'checkbox'],
+                'help_attr' => ['class' => 'help'],
             ])
             ->add('newPassword', RepeatedType::class, [
                 'mapped' => false,
@@ -61,7 +89,10 @@ class UserType extends AbstractType
                     'label' => 'Mot de passe',
                     'help' => 'Vous mettez à jour un compte ? laissez vide pour ne pas changer',
                     'toggle' => true,
-                    'attr' => ['autocomplete' => 'new-password'],
+                    'attr' => ['autocomplete' => 'new-password', 'class' => 'input'],
+                    'label_attr' => ['class' => 'label'],
+                    'row_attr' => ['class' => 'field'],
+                    'help_attr' => ['class' => 'help'],
                     'constraints' => [
                         new Length([
                             'min' => 6,
@@ -73,7 +104,10 @@ class UserType extends AbstractType
                 'second_options' => [
                     'label' => 'Répétez le mot de passe',
                     'toggle' => true,
-                    'attr' => ['autocomplete' => 'new-password'],
+                    'attr' => ['autocomplete' => 'new-password', 'class' => 'input'],
+                    'label_attr' => ['class' => 'label'],
+                    'row_attr' => ['class' => 'field'],
+                    'help_attr' => ['class' => 'help'],
                     ],
             ])
             /*->add('lastUpdate')

+ 45 - 0
src/Repository/GameRepository.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\Game;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<Game>
+ */
+class GameRepository extends ServiceEntityRepository
+{
+    public function __construct(ManagerRegistry $registry)
+    {
+        parent::__construct($registry, Game::class);
+    }
+
+    //    /**
+    //     * @return Genre[] Returns an array of Genre objects
+    //     */
+    //    public function findByExampleField($value): array
+    //    {
+    //        return $this->createQueryBuilder('g')
+    //            ->andWhere('g.exampleField = :val')
+    //            ->setParameter('val', $value)
+    //            ->orderBy('g.id', 'ASC')
+    //            ->setMaxResults(10)
+    //            ->getQuery()
+    //            ->getResult()
+    //        ;
+    //    }
+
+    //    public function findOneBySomeField($value): ?Genre
+    //    {
+    //        return $this->createQueryBuilder('g')
+    //            ->andWhere('g.exampleField = :val')
+    //            ->setParameter('val', $value)
+    //            ->getQuery()
+    //            ->getOneOrNullResult()
+    //        ;
+    //    }
+}
+
+?>

+ 144 - 0
src/Service/PictureService.php

@@ -0,0 +1,144 @@
+<?php
+
+namespace App\Service;
+
+use Exception;
+use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
+use Symfony\Component\HttpFoundation\File\UploadedFile;
+
+class PictureService
+{
+    public function __construct(private ParameterBagInterface $params)
+    {
+
+    }
+
+    // Redimensionner en carré 250x250
+    public function square(UploadedFile $picture, ?string $folder = '', ?string $newName): string
+    {
+        // Dimensions de la vignette
+        $width = 250;
+        $height = $width;
+        
+        // On donne un nouveau nom à l'image
+        $file = $newName . '.webp';
+
+        // On récupère les informations de l'image
+        $pictureInfos = getimagesize($picture);
+
+        if($pictureInfos === false){
+            throw new Exception('Format d\'image incorrect');
+        }
+
+        // On vérifie le type mime
+        switch($pictureInfos['mime']){
+            case 'image/png':
+                $sourcePicture = imagecreatefrompng($picture);
+                break;
+            case 'image/jpeg':
+                $sourcePicture = imagecreatefromjpeg($picture);
+                break;
+            case 'image/webp':
+                $sourcePicture = imagecreatefromwebp($picture);
+                break;
+            default:
+                throw new Exception('Format d\'image incorrect');
+        }
+
+        // On recadre l'image
+        $imageWidth = $pictureInfos[0];
+        $imageHeight = $pictureInfos[1];
+
+        switch($imageWidth <=> $imageHeight){
+            case -1: // portrait
+                $squareSize = $imageWidth;
+                $srcX = 0;
+                $srcY = ($imageHeight - $imageWidth) / 2;
+                break;
+
+            case 0: // carré
+                $squareSize = $imageWidth;
+                $srcX = 0;
+                $srcY = 0;
+                break;
+
+            case 1: // paysage
+                $squareSize = $imageHeight;
+                $srcX = ($imageWidth - $imageHeight) / 2;
+                $srcY = 0;
+                break;
+        }
+
+        // On crée une nouvelle image vierge
+        $resizedPicture = imagecreatetruecolor($width, $width);
+
+        // On génère le contenu de l'image
+        imagecopyresampled($resizedPicture, $sourcePicture, 0, 0, $srcX, $srcY, $width, $width, $squareSize, $squareSize);
+
+        // On crée le chemin de stockage
+        $path = $this->params->get('upload_images_directory') . $folder;
+
+        // On stocke l'image réduite
+        imagewebp($resizedPicture, $path . $file);
+
+        return $file;
+    }
+
+    // Redientionner en bannière 600x200
+    public function banner(UploadedFile $picture, ?string $folder = '', ?string $newName): string
+    {
+         // Dimensions de la vignette
+        $width = 600;
+        $height = 200;
+        
+        // On donne un nouveau nom à l'image
+        $file = $newName . '.webp';
+
+        // On récupère les informations de l'image
+        $pictureInfos = getimagesize($picture);
+
+        if($pictureInfos === false){
+            throw new Exception('Format d\'image incorrect');
+        }
+
+        // On vérifie le type mime
+        switch($pictureInfos['mime']){
+            case 'image/png':
+                $sourcePicture = imagecreatefrompng($picture);
+                break;
+            case 'image/jpeg':
+                $sourcePicture = imagecreatefromjpeg($picture);
+                break;
+            case 'image/webp':
+                $sourcePicture = imagecreatefromwebp($picture);
+                break;
+            default:
+                throw new Exception('Format d\'image incorrect');
+        }
+
+        // On recadre l'image
+        $imageWidth = $pictureInfos[0];
+        $imageHeight = $pictureInfos[1];
+
+        $newWidth = $imageWidth;
+        $newHeight = ($newWidth / $width) * $height;
+        $srcY = ($imageHeight - $newHeight) / 2;
+
+        // On crée une nouvelle image vierge
+        $resizedPicture = imagecreatetruecolor($width, $height);
+
+        // On génère le contenu de l'image
+        imagecopyresampled($resizedPicture, $sourcePicture, 0, 0, 0, $srcY, $width, $height, $newWidth, $newHeight);
+
+        // On crée le chemin de stockage
+        $path = $this->params->get('upload_images_directory') . $folder;
+
+        // On stocke l'image réduite
+        imagewebp($resizedPicture, $path . $file);
+
+
+        return $file;
+    }
+}
+
+?>

+ 111 - 0
templates/admin/game/edit.html.twig

@@ -0,0 +1,111 @@
+{% extends 'bulma.html.twig' %}
+
+{% block title %}Administration > Jeu > Éditer{% endblock %}
+
+{% block content %}
+
+    {{ form_errors(form) }}
+    {{ form_start(form) }}
+
+    {{ form_row(form.name) }}
+    {{ form_row(form.slug) }}
+    {{ form_row(form.description)}}
+
+    {# gestion de l'illustration #}
+    <div class="box">
+    <div class="field">
+        <div class="columns">
+            <div class="column">
+                {{ form_label(form.picture) }}
+                <div class="file has-name is-fullwidth" id="file-js"  {{ stimulus_controller('bulma-filenames') }}>
+                <label class="file-label" >
+                    {{ form_widget(form.picture) }}
+                    <span class="file-cta">
+                    <span class="file-icon">
+                        <twig:ux:icon name="bi:cloud-upload" />
+                    </span>
+                    <span class="file-label"> Choisissez un fichier… </span>
+                    </span>
+                    <span class="file-name"> aucun fichier </span>
+                </label>
+                </div>
+                {{ form_help(form.picture) }}
+                {% if game.picture %}
+                <div class="field mt-2">
+                <p><a href="{{ path('app_admin_game_del_pic', {id: game.id}) }}" class="button is-danger"><twig:ux:icon name="bi:trash-fill" class="small-icon-in-text"/> Supprimer l'image chargée.</a></p>
+                </div>
+                {% endif %}
+                
+
+            </div>
+            <div class="column">
+                {% if game.picture %}
+                <img src="/images/games/{{ game.picture }}" class="image is-3by1"/>
+                {% endif %}
+            </div>
+        </div>
+    </div>
+    </div>
+
+    <div class="box">
+    <div class="field">
+        {{ form_label(form.genre) }}
+        {{ form_widget(form.genre) }}
+        {{ form_help(form.genre) }}
+    </div>
+    </div>
+
+    <div class="columns">
+        <div class="column">
+            <div class="box" {{ stimulus_controller('admin_game') }}>
+                <h3 class="title is-5">Support du jeu</h3>
+                <div class="field" id="library-controller">
+                    {{ form_widget(form.isInAssoLibrary) }}
+                    {{ form_label(form.isInAssoLibrary) }}
+                    {{ form_help(form.isInAssoLibrary) }}
+                </div>
+                <div class="field only-in-library {% if not game.isInAssoLibrary %}is-hidden{% endif %}">
+                    {{ form_widget(form.isPhysical) }}
+                    {{ form_label(form.isPhysical) }}
+                    {{ form_help(form.isPhysical) }}
+                </div>
+                <div class="field only-in-library {% if not game.isInAssoLibrary %}is-hidden{% endif %}">
+                    {{ form_widget(form.isNumerical) }}
+                    {{ form_label(form.isNumerical) }}
+                    {{ form_help(form.isNumerical) }}
+                </div>
+                <div class="field only-in-library {% if not game.isInAssoLibrary %}is-hidden{% endif %}">
+                    {{ form_label(form.urlNumericalVersion) }}
+                    {{ form_widget(form.urlNumericalVersion) }}
+                    {{ form_help(form.urlNumericalVersion) }}
+                </div>   
+            </div>
+        </div>
+        <div class="column">
+            <div class="box">
+                <h3 class="title is-5">Gestion de la fiche</h3>
+                {{ form_row(form.addBy) }}
+                {{ form_row(form.addDatetime) }}
+                <div class="field">
+                    {{ form_widget(form.isValidByAdmin) }}
+                    {{ form_label(form.isValidByAdmin) }}
+                    {{ form_help(form.isValidByAdmin) }}
+                </div>
+
+                {{ form_widget(form) }}
+            </div>
+        </div>
+    </div>
+    
+
+
+
+
+    <div class="control">
+        <button class="button is-primary" type="submit">Envoyer</button>
+        <a href="{{ path('app_admin_game') }}" class="button">Annuler</a>
+    </div>
+
+    {{ form_end(form) }}
+
+{% endblock %}

+ 52 - 0
templates/admin/game/index.html.twig

@@ -0,0 +1,52 @@
+{% extends 'bulma.html.twig' %}
+
+{% block title %}Administration > Games{% endblock %}
+
+{% block content %}
+
+    <nav class="breadcrumb has-arrow-separator" aria-label="breadcrumbs">
+      <ul>
+        <li><a href="{{ path('app_main') }}">Accueil</a></li>
+        <li><a href="{{ path('app_admin') }}">Administration</a></li>
+        <li class="is-active"><a href="{{ path('app_admin_game') }}">Gestion des jeux</a></li>
+      </ul>
+    </nav>
+
+    <div class="block">
+        <h1 class="title">Gestion des jeux</h1>
+        <p class="subtitle">Liste de jeux de rôle connus dans l'application.</p>
+    </div>
+
+    <div class="block">
+        <div class="is-grouped">
+            <a class="button is-primary" href="{{ path('app_admin_game_add') }}">Ajouter un jeu</a>
+        </div>
+    </div>
+
+    <div class="block">
+        <table id="datatable" {{ stimulus_controller('datatables') }} class="table is-striped is-hoverable is-fullwidth">
+            <thead>
+            <tr>
+                <th>Nom du jeu</th>
+                <th>Validé?</th>
+                <th>Actions</th>
+            </tr>
+            </thead>
+            <tbody>
+            {% for game in games %}
+                <tr>
+                    <td><a href="{{ path('app_admin_game_edit', {id: game.id}) }}">{{ game.name }}</a></td>
+                    <td>{{ game.isValidByAdmin }}</td>
+                    <td>
+                        <a class="button" href="{{ path('app_admin_game_edit', {id: game.id}) }}">Éditer</a>
+                        <a class="button is-danger" data-id="{{ path('app_admin_game_delete', {'id': game.id})}}" href="#" {{ stimulus_controller('admin_confirm') }}>Supprimer</a>
+                    </td>
+                </tr>
+            {% endfor %}
+            </tbody>
+
+        </table>
+    </div>
+
+
+{% endblock %}

+ 4 - 34
templates/admin/genre/edit.html.twig

@@ -6,42 +6,12 @@
 
     {{ form_errors(form) }}
     {{ form_start(form) }}
+    {{ form_widget(form) }}
 
-        <div class="field is-horizontal">
-            <div class="field-label is-normal">
-            {{ form_label(form.genre, null, {'label_attr': {'class': 'label'}}) }}
-            </div>
-            <div class="field-body">
-            {{ form_widget(form.genre, {'attr': {'class': 'input'}}) }}
-            </div>
-        </div>
-        
-        <div class="field is-horizontal">
-            <div class="field-label is-normal">
-            {{ form_label(form.slug, null, {'label_attr': {'class': 'label'}}) }}         
-            </div>  
-            <div class="field-body">
-                <div class="field is-expanded">
-                    <p>{{ form_widget(form.slug, {'attr': {'class': 'input'}}) }}</p>
-                    {# <span class="icon is-danger">{{ ux_icon('bi:exclamation-circle-fill') }}</span>  #}
-                    <small class="help">{{ form_help(form.slug) }}</small>
-                </div>
-            </div>
-        </div>
-
-
-        {{ form_widget(form) }}
-
-        <div class="field is-horizontal">
-            <div class="field-label is-normal"></div>
-            <div class="field-body">
-                <div class="control">
-        <button class="button is-primary" type="submit">Envoyer</button></div>
-        <div class='control'>
+    <div class="control">
+        <button class="button is-primary" type="submit">Envoyer</button>
         <a href="{{ path('app_admin_genre') }}" class="button">Annuler</a>
-        </div>
-        </div>
-        </div>
+    </div>
     {{ form_end(form) }}
 
 {% endblock %}

+ 5 - 3
templates/admin/genre/index.html.twig

@@ -27,16 +27,18 @@
         <table id="datatable" {{ stimulus_controller('datatables') }} class="table is-striped is-hoverable is-fullwidth">
             <thead>
             <tr>
-                <th>Slug</th>
                 <th>Genre</th>
+                <th>Slug</th>
+                <th>Nombre de jeux</th>
                 <th>Actions</th>
             </tr>
             </thead>
             <tbody>
             {% for genre in genres %}
                 <tr>
-                    <td><a href="{{ path('app_admin_genre_edit', {id: genre.id}) }}">{{ genre.slug }}</a></td>
-                    <td>{{ genre.genre }}</td>
+                    <td><a href="{{ path('app_admin_genre_edit', {id: genre.id}) }}">{{ genre.genre }}</a></td>
+                    <td>{{ genre.slug }}</td>
+                    <td>{{ genre.howManyGames }}</td>
                     <td>
                         <a class="button" href="{{ path('app_admin_genre_edit', {id: genre.id}) }}">Éditer</a>
                         <a class="button is-danger" data-id="{{ path('app_admin_genre_delete', {'id': genre.id})}}" href="#" {{ stimulus_controller('admin_confirm') }}>Supprimer</a>

+ 1 - 1
templates/admin/index.html.twig

@@ -57,7 +57,7 @@
           <div class="box">
             <h2 class="title is-4">Jeux</h2>
             <p>Gérer les jeux et les ludothèques de l'association.</p>
-            <p><a href="#">Gérer les jeux</a></p>
+            <p><a href="{{ path('app_admin_game') }}">Gérer les jeux</a></p>
           </div>
         </div>
         <!-- Colonne 3 -->

+ 16 - 98
templates/admin/user/edit.html.twig

@@ -7,115 +7,33 @@
     {{ form_errors(form) }}
     {{ form_start(form) }}
 
-        <div class="field is-horizontal">
-            <div class="field-label is-normal">
-            {{ form_label(form.firstName, null, {'label_attr': {'class': 'label'}}) }}
-            </div>
-            <div class="field-body">
-            {{ form_widget(form.firstName, {'attr': {'class': 'input'}}) }}
-            </div>
-        </div>
-        
-        <div class="field is-horizontal">
-            <div class="field-label is-normal">
-            {{ form_label(form.lastName, null, {'label_attr': {'class': 'label'}}) }}
-            </div>
-            <div class="field-body">
-            {{ form_widget(form.lastName, {'attr': {'class': 'input'}}) }}
-            </div>
-        </div>
-        
-        <div class="field is-horizontal">
-            <div class="field-label is-normal">
-            {{ form_label(form.phone, null, {'label_attr': {'class': 'label'}}) }}         
-            </div>  
-            <div class="field-body">
-                <div class="field is-expanded">
-                    <p>{{ form_widget(form.phone, {'attr': {'class': 'input'}}) }}</p>
-                    {# <span class="icon is-danger">{{ ux_icon('bi:exclamation-circle-fill') }}</span>  #}
-                    {% if form_errors(form.phone) %}<small class="help is-danger">{{ form_errors(form.phone) }}</small>{% endif %}
-                    <small class="help">{{ form_help(form.phone) }}</small>
-                </div>
-            </div>
-        </div>
-
-        <div class="field is-horizontal">
-            <div class="field-label is-normal">
-            {{ form_label(form.email, null, {'label_attr': {'class': 'label'}}) }}         
-            </div>  
-            <div class="field-body">
-                <div class="field is-expanded">
-                    <p>{{ form_widget(form.email, {'attr': {'class': 'input'}}) }}</p>
-                    {# <span class="icon is-danger">{{ ux_icon('bi:exclamation-circle-fill') }}</span>  #}
-                    {% if form_errors(form.email) %}<small class="help is-danger">{{ form_errors(form.email) }}</small>{% endif %}
-                    <small class="help">{{ form_help(form.email) }}</small>
-                </div>
-            </div>
-        </div>
+        {{ form_row(form.firstName) }}
+        {{ form_row(form.lastName) }}
+        {{ form_row(form.email) }}
 
-        <div class="field is-horizontal">
-            <div class="field-label is-normal">
-                {{ form_label(form.isVerified, null, {'label_attr': {'class': 'label'}}) }}
-            </div>  
-            <div class="field-body">
-                <div class="field is-horizontal">
-                    <p>{{ form_widget(form.isVerified, {'attr': {'class': 'checkbox'}}) }}
-                    &nbsp;<small class="is-help">{{ form_help(form.isVerified) }}</small></p>
-                </div>
-            </div>
-        </div>
-        
-        <div class="field is-horizontal">
-            <div class="field-label is-normal">
-                {{ form_label(form.roles, null, {'label_attr': {'class': 'label'}}) }}
-            </div>  
-            <div class="field-body">
-                <div class="field is-horizontal">
-                    {{ form_widget(form.roles, {'attr': {'class': 'checkbox'}}) }}
-                </div>
-            </div>
+        <div class="field">
+            {{ form_widget(form.isVerified) }}
+            {{ form_label(form.isVerified) }}
+            {{ form_help(form.isVerified) }}
         </div>
 
-        <div class="field is-horizontal">
-            <div class="field-label is-normal">
-            {{ form_label(form.newPassword.first, null, {'label_attr': {'class': 'label'}}) }}         
-            </div>  
-            <div class="field-body">
-                <div class="field is-expanded">
-                    <p>{{ form_widget(form.newPassword.first, {'attr': {'class': 'input'}}) }}</p>
+        {{ form_row(form.phone) }}
 
-                    {% if form_errors(form.newPassword.first) %}<small class="help is-danger">{{ form_errors(form.newPassword.first) }}</small>{% endif %}
-                    <small class="help">{{ form_help(form.newPassword.first) }}</small>
-                </div>
-            </div>
-        </div>
-        
-        <div class="field is-horizontal">
-            <div class="field-label is-normal">
-            {{ form_label(form.newPassword.second, null, {'label_attr': {'class': 'label'}}) }}         
-            </div>  
-            <div class="field-body">
-                <div class="field is-expanded">
-                    <p>{{ form_widget(form.newPassword.second, {'attr': {'class': 'input'}}) }}</p>
+        <div class="field">
+            {{ form_label(form.roles) }}
 
-                    {% if form_errors(form.newPassword.second) %}<small class="help is-danger">{{ form_errors(form.newPassword.second) }}</small>{% endif %}
-                    <small class="help">{{ form_help(form.newPassword.second) }}</small>
-                </div>
-            </div>
+                {{ form_widget(form.roles) }}
+            {{ form_help(form.roles) }}
         </div>
 
         {{ form_widget(form) }}
 
-        <div class="field is-horizontal">
-            <div class="field-label is-normal"></div>
-            <div class="field-body">
-                <div class="control">
-        <button class="button is-primary" type="submit">Envoyer</button></div>
-        <div class='control'>
+        <div class="control">
+        <button class="button is-primary" type="submit">Envoyer</button>
+  
         <a href="{{ path('app_admin_user') }}" class="button">Annuler</a>
         </div>
-        </div>
-        </div>
+
     {{ form_end(form) }}
 
 {% endblock %}

+ 1 - 1
templates/admin/user/index.html.twig

@@ -38,7 +38,7 @@
             <tbody>
             {% for user in users %}
                 <tr>
-                    <td><a href="{{ path('app_admin_user_edit', {id: user.id}) }}">{{ user.firstname }} {{ user.lastname }}</a></td>
+                    <td><a href="{{ path('app_admin_user_edit', {id: user.id}) }}">{{ user.fullName }}</a></td>
                     <td><span class="icon-text">{{ user.email }}{% if user.isVerified %}<span class="icon"><twig:ux:icon name="bi:check2-circle" /></span>{% else %}<a href="{{ path('app_admin_user_resend_verification_email', {id: user.id}) }}" title="Renvoyer l'email de confirmation"><span class="icon"><twig:ux:icon name="bi:arrow-clockwise" /></span></a>{% endif %}</span></td>
                     <td>{{ component('Role', {roles: user.roles}) }}</td>
                     <td>{{ user.lastLogin ? user.lastLogin|date('d/m/Y H:i:s', app_timezone) : 'Jamais' }}</td>