소스 검색

gestion des jeux, des mj et des utilisateurs

garthh 2 일 전
부모
커밋
6af48c73b6
36개의 변경된 파일1217개의 추가작업 그리고 64개의 파일을 삭제
  1. 39 0
      migrations/Version20250730073654.php
  2. 35 0
      migrations/Version20250730100757.php
  3. 35 0
      migrations/Version20250730100906.php
  4. 31 0
      migrations/Version20250730143023.php
  5. 31 0
      migrations/Version20250730153951.php
  6. 31 0
      migrations/Version20250730154411.php
  7. BIN
      public/images/gamemasters/mj-de-poche.webp
  8. BIN
      public/images/games/bgs.webp
  9. 18 4
      src/Controller/Admin/GameController.php
  10. 175 0
      src/Controller/Admin/GamemasterController.php
  11. 40 0
      src/Controller/Admin/UserController.php
  12. 2 1
      src/Controller/ResetPasswordController.php
  13. 35 0
      src/Entity/Game.php
  14. 228 0
      src/Entity/Gamemaster.php
  15. 26 1
      src/Entity/User.php
  16. 12 4
      src/Form/ChangePasswordFormType.php
  17. 0 2
      src/Form/GameType.php
  18. 126 0
      src/Form/GamemasterType.php
  19. 36 12
      src/Form/RegistrationFormType.php
  20. 7 1
      src/Form/ResetPasswordRequestFormType.php
  21. 0 4
      src/Form/UserType.php
  22. 61 0
      src/Repository/GamemasterRepository.php
  23. 10 0
      src/Repository/UserRepository.php
  24. 1 1
      templates/admin/game/index.html.twig
  25. 83 0
      templates/admin/gamemaster/edit.html.twig
  26. 55 0
      templates/admin/gamemaster/index.html.twig
  27. 1 1
      templates/admin/index.html.twig
  28. 2 0
      templates/admin/user/index.html.twig
  29. 2 0
      templates/bulma.html.twig
  30. 0 2
      templates/main/markdown.html.twig
  31. 33 10
      templates/registration/register.html.twig
  32. 16 5
      templates/reset_password/check_email.html.twig
  33. 24 6
      templates/reset_password/email.html.twig
  34. 6 0
      templates/reset_password/email.txt.twig
  35. 15 9
      templates/reset_password/request.html.twig
  36. 1 1
      templates/reset_password/reset.html.twig

+ 39 - 0
migrations/Version20250730073654.php

@@ -0,0 +1,39 @@
+<?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 Version20250730073654 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 gamemaster (id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', link_to_user_id BINARY(16) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', email VARCHAR(180) NOT NULL, first_name VARCHAR(180) DEFAULT NULL, last_name VARCHAR(180) DEFAULT NULL, prefered_name VARCHAR(180) DEFAULT NULL, description LONGTEXT DEFAULT NULL, phone VARCHAR(255) DEFAULT NULL, UNIQUE INDEX UNIQ_950236C57E7C2E95 (link_to_user_id), UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+        $this->addSql('CREATE TABLE gamemaster_game (gamemaster_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', game_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', INDEX IDX_85092B7096376157 (gamemaster_id), INDEX IDX_85092B70E48FD905 (game_id), PRIMARY KEY(gamemaster_id, game_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+        $this->addSql('ALTER TABLE gamemaster ADD CONSTRAINT FK_950236C57E7C2E95 FOREIGN KEY (link_to_user_id) REFERENCES user (id)');
+        $this->addSql('ALTER TABLE gamemaster_game ADD CONSTRAINT FK_85092B7096376157 FOREIGN KEY (gamemaster_id) REFERENCES gamemaster (id) ON DELETE CASCADE');
+        $this->addSql('ALTER TABLE gamemaster_game ADD CONSTRAINT FK_85092B70E48FD905 FOREIGN KEY (game_id) REFERENCES game (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 gamemaster DROP FOREIGN KEY FK_950236C57E7C2E95');
+        $this->addSql('ALTER TABLE gamemaster_game DROP FOREIGN KEY FK_85092B7096376157');
+        $this->addSql('ALTER TABLE gamemaster_game DROP FOREIGN KEY FK_85092B70E48FD905');
+        $this->addSql('DROP TABLE gamemaster');
+        $this->addSql('DROP TABLE gamemaster_game');
+    }
+}

+ 35 - 0
migrations/Version20250730100757.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 Version20250730100757 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 gamemaster DROP FOREIGN KEY FK_950236C57E7C2E95');
+        $this->addSql('DROP INDEX UNIQ_950236C57E7C2E95 ON gamemaster');
+        $this->addSql('ALTER TABLE gamemaster DROP link_to_user_id');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE gamemaster ADD link_to_user_id BINARY(16) DEFAULT NULL COMMENT \'(DC2Type:uuid)\'');
+        $this->addSql('ALTER TABLE gamemaster ADD CONSTRAINT FK_950236C57E7C2E95 FOREIGN KEY (link_to_user_id) REFERENCES user (id)');
+        $this->addSql('CREATE UNIQUE INDEX UNIQ_950236C57E7C2E95 ON gamemaster (link_to_user_id)');
+    }
+}

+ 35 - 0
migrations/Version20250730100906.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 Version20250730100906 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 gamemaster ADD link_to_user_id BINARY(16) DEFAULT NULL COMMENT \'(DC2Type:uuid)\'');
+        $this->addSql('ALTER TABLE gamemaster ADD CONSTRAINT FK_950236C57E7C2E95 FOREIGN KEY (link_to_user_id) REFERENCES user (id)');
+        $this->addSql('CREATE UNIQUE INDEX UNIQ_950236C57E7C2E95 ON gamemaster (link_to_user_id)');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE gamemaster DROP FOREIGN KEY FK_950236C57E7C2E95');
+        $this->addSql('DROP INDEX UNIQ_950236C57E7C2E95 ON gamemaster');
+        $this->addSql('ALTER TABLE gamemaster DROP link_to_user_id');
+    }
+}

+ 31 - 0
migrations/Version20250730143023.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 Version20250730143023 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 gamemaster ADD picture VARCHAR(255) DEFAULT NULL');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE gamemaster DROP picture');
+    }
+}

+ 31 - 0
migrations/Version20250730153951.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 Version20250730153951 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 gamemaster ADD slug VARCHAR(255) NOT NULL');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE gamemaster DROP slug');
+    }
+}

+ 31 - 0
migrations/Version20250730154411.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 Version20250730154411 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 gamemaster CHANGE slug slug VARCHAR(255) DEFAULT NULL');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE gamemaster CHANGE slug slug VARCHAR(255) NOT NULL');
+    }
+}

BIN
public/images/gamemasters/mj-de-poche.webp


BIN
public/images/games/bgs.webp


+ 18 - 4
src/Controller/Admin/GameController.php

@@ -38,8 +38,8 @@ final class GameController extends AbstractController
     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)) {
+        if ($game->getPicture()) {
+            $fullPath = $params->get('upload_images_directory') . '/games/' . $game->getPicture();
             unlink($fullPath);
         }
         
@@ -57,8 +57,8 @@ final class GameController extends AbstractController
     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)) {
+        if ($game->getPicture()) {
+            $fullPath = $params->get('upload_images_directory') . '/games/' . $game->getPicture();
             unlink($fullPath);
         }
         $game->setPicture(null);
@@ -68,6 +68,20 @@ final class GameController extends AbstractController
         return $this->redirectToRoute('app_admin_game_edit', ['id' => $game->getId()]);
     }
 
+    /*
+     * Valider le jeu
+     */
+    #[Route('/admin/game/{id}/valid', name: 'app_admin_game_valid', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
+    public function valid(?Game $game, EntityManagerInterface $manager): Response
+    {
+        $game->setIsValidByAdmin(true);
+        $game->setValidDatetime(new \Datetime('now'));
+        $manager->persist($game);
+        $manager->flush();
+        $this->addFlash('success', 'Le jeu est validé.');
+        return $this->redirectToRoute('app_admin_game');
+    }
+
     /* 
      * Modifier un jeu
      */

+ 175 - 0
src/Controller/Admin/GamemasterController.php

@@ -0,0 +1,175 @@
+<?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\Gamemaster;
+use App\Form\GamemasterType;
+use App\Service\PictureService;
+use App\Repository\UserRepository;
+use App\Repository\GamemasterRepository;
+
+final class GamemasterController extends AbstractController
+{
+    #[Route('/admin/gamemaster', name: 'app_admin_gm', methods: ['GET'])]
+    public function index(GamemasterRepository $repository): Response
+    {
+        $gamemasters = $repository->findAll();
+
+        return $this->render('admin/gamemaster/index.html.twig', [
+            'gamemasters' => $gamemasters
+        ]);
+    }
+
+    /*
+     * Supprimer un jeu
+     */
+    #[Route('/admin/gamemaster/{id}/delete', name: 'app_admin_gamemaster_delete', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
+    public function delete(?Gamemaster $gamemaster, EntityManagerInterface $manager, ParameterBagInterface $params): Response
+    {
+        // Effacer toutes images associée
+        if ($gamemaster->getPicture()) {
+            $fullPath = $params->get('upload_images_directory') . '/gamemasters/' . $gamemaster->getPicture();
+            unlink($fullPath);
+        }
+        
+        $manager->remove($gamemaster);
+        $manager->flush();
+        
+        $this->addFlash('success', 'Meneur de jeu supprimé avec succès.');
+        return $this->redirectToRoute('app_admin_gm');
+    }
+
+    /*
+     * Supprimer l'image un jeu
+     */
+    #[Route('/admin/gamemaster/{id}/del-pic', name: 'app_admin_gamemaster_del_pic', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
+    public function deletePicture(?Gamemaster $gamemaster, EntityManagerInterface $manager, ParameterBagInterface $params): Response
+    {
+        // Effacer toutes images associée
+        if ($gamemaster->getPicture()) {
+            $fullPath = $params->get('upload_images_directory') . '/games/' . $gamemaster->getPicture();
+            unlink($fullPath);
+        }
+        $gamemaster->setPicture(null);
+        $manager->flush();
+        
+        $this->addFlash('success', 'Illustration supprimée avec succès.');
+        return $this->redirectToRoute('app_admin_gamemaster_edit', ['id' => $gamemaster->getId()]);
+    }
+
+    /* 
+     * Modifier un jeu
+     */
+    #[Route('/admin/gamemaster/{id}/edit', name: 'app_admin_gamemaster_edit', requirements: ['id' => Requirement::UUID_V7], methods: ['GET', 'POST'])]
+    public function edit(?Gamemaster $gamemaster, Request $request, EntityManagerInterface $manager, PictureService $pictureService): Response
+    {
+        $form = $this->createForm(GamemasterType::class, $gamemaster);
+
+        $form->handleRequest($request);
+        if ($form->isSubmitted() && $form->isValid()) {
+
+            if (!$form->get('preferedName')->getData()) {
+                $gamemaster->setPreferedName($form->get('firstName')->getData() ." ". $form->get('lastName')->getData());
+            }
+
+            $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($gamemaster->getPreferedName()));
+                $gamemaster->setSlug($slug);
+            }
+
+            
+            // Traiter l'image proposée
+            $tmpPicture = $form->get('picture')->getData();
+            if ($tmpPicture) {
+                $picture = $pictureService->square($tmpPicture, '/gamemasters/', $slug);
+                $gamemaster->setPicture($picture);
+            }
+            $manager->persist($gamemaster);
+            $manager->flush();
+
+            $this->addFlash('success', 'Meneur de jeu modifié avec succès.');
+            return $this->redirectToRoute('app_admin_gm');
+        }
+
+        return $this->render('admin/gamemaster/edit.html.twig', [
+            'form' => $form,
+            'gamemaster' => $gamemaster,
+        ]);
+    }
+
+    /*
+     * Associer les MJ à leurs comptes utlisateurs s'il existe
+     */
+    #[Route('/admin/gamemaster/link', name: 'app_admin_gamemaster_link', methods: ['GET'])]
+    public function link(EntityManagerInterface $manager, UserRepository $userRepo, GamemasterRepository $gmRepo): Response
+    {
+        $linkedAccount = 0;
+        foreach ($gmRepo->findUnlinked() as $gamemaster) {
+            $lookUpUser = $userRepo->findByEmail($gamemaster->getEmail());
+            if ($lookUpUser) {
+                $gamemaster->setLinkToUser($lookUpUser);
+                $manager->persist($gamemaster);
+                $manager->flush();
+                $linkedAccount++;
+            }
+        }
+        $this->addFlash('success', $linkedAccount.' comptes associés');
+        return $this->redirectToRoute('app_admin_gm');
+    }
+
+    /* 
+     * Ajouter un meneur de jeu
+     */
+    #[Route('/admin/gamemaster/add', name: 'app_admin_gamemaster_add', methods: ['GET', 'POST'])]
+    public function add(Request $request, EntityManagerInterface $manager, PictureService $pictureService): Response
+    {
+        $gamemaster = new Gamemaster();
+        $form = $this->createForm(GamemasterType::class, $gamemaster);
+
+        $form->handleRequest($request);
+        if ($form->isSubmitted() && $form->isValid()) {
+
+            if (!$form->get('preferedName')->getData()) {
+                $gamemaster->setPreferedName($form->get('firstName')->getData() ." ". $form->get('lastName')->getData());
+            }
+
+            $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($gamemaster->getPreferedName()));
+                $gamemaster->setSlug($slug);
+            }
+
+            
+            // Traiter l'image proposée
+            $tmpPicture = $form->get('picture')->getData();
+            if ($tmpPicture) {
+                $picture = $pictureService->square($tmpPicture, '/gamemasters/', $slug);
+                $gamemaster->setPicture($picture);
+            }
+            $manager->persist($gamemaster);
+            $manager->flush();
+
+            $this->addFlash('success', 'Meneur de jeu ajouté avec succès.');
+            return $this->redirectToRoute('app_admin_gm');
+        }
+
+        return $this->render('admin/gamemaster/edit.html.twig', [
+            'form' => $form,
+            'gamemaster' => $gamemaster,
+        ]);
+    }
+}

+ 40 - 0
src/Controller/Admin/UserController.php

@@ -14,10 +14,13 @@ use Symfony\Bridge\Twig\Mime\TemplatedEmail;
 use Symfony\Component\Mime\Address;
 use Symfony\Component\Mailer\MailerInterface;
 use Symfony\Component\Mime\Email;
+use Symfony\Component\String\Slugger\AsciiSlugger;
 
 use App\Entity\User;
+use App\Entity\Gamemaster;
 use App\Form\UserType;
 use App\Repository\UserRepository;
+use App\Repository\GamemasterRepository;
 
 final class UserController extends AbstractController
 {
@@ -46,12 +49,49 @@ final class UserController extends AbstractController
     #[Route('/admin/user/{id}/delete', name: 'app_admin_user_delete', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
     public function delete(?User $user, EntityManagerInterface $manager): Response
     {
+        // Si MJ, supprimer le lien avant de supprimer
+        if ($user->getLinkToGamemaster()) {
+            $user->setLinkToGamemaster(null);
+            $manager->persist($user);
+        }
+
         $manager->remove($user);
         $manager->flush();
         $this->addFlash('success', 'Utilisateur supprimé avec succès.');
         return $this->redirectToRoute('app_admin_user');
     }
 
+    /*
+     * Créer un MJ à partir d'un utitlisateur
+     */
+    #[Route('/admin/user/{id}/create_gamemaster', name: 'app_admin_user_create_gm', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
+    public function createGamemaster(?User $user, EntityManagerInterface $manager, GamemasterRepository $gmRepo): Response
+    {
+        if ($gmRepo->findByEmail($user->getEmail())) {
+            $this->addFlash('danger', 'Un profil meneur(euse) de jeu existe déjà avec cet email. Associez le depuis la gestion des meneur(euse)s de jeux.');
+            return $this->redirectToRoute('app_admin_user');
+        }
+        if ($user->getLinkToGamemaster()) {
+            $this->addFlash('danger', 'Profil meneur(euse) de jeu déjà existant.');
+            return $this->redirectToRoute('app_admin_user');
+        }
+        $gamemaster = new Gamemaster();
+        $gamemaster->setFirstName($user->getFirstName());
+        $gamemaster->setLastName($user->getLastName());
+        $gamemaster->setPreferedName($user->getFullName());
+        $slugger = new AsciiSlugger('fr_FR');
+        $slug = $slugger->slug(strtolower($user->getFullName()));
+        $gamemaster->setSlug($slug);
+        $gamemaster->setEmail($user->getEmail());
+        $phone = $user->getPhone();
+        if ($phone) { $gamemaster->setPhone($phone); }
+        $gamemaster->setLinkToUser($user);
+        $manager->persist($gamemaster);
+        $manager->flush();
+        $this->addFlash('success', 'Meneur.euse de jeu créé.e avec succès.');
+        return $this->redirectToRoute('app_admin_user');
+    }
+
     /*
      * Modifier un utilisateur
      */

+ 2 - 1
src/Controller/ResetPasswordController.php

@@ -159,8 +159,9 @@ class ResetPasswordController extends AbstractController
         $email = (new TemplatedEmail())
             ->from(new Address($_ENV['CONTACT_EMAIL'], $_ENV['CONTACT_NAME']))
             ->to((string) $user->getEmail())
-            ->subject('Your password reset request')
+            ->subject('Votre demande de réinitialisation de mot de passe')
             ->htmlTemplate('reset_password/email.html.twig')
+            ->textTemplate('reset_password/email.txt.twig')
             ->context([
                 'resetToken' => $resetToken,
             ])

+ 35 - 0
src/Entity/Game.php

@@ -65,10 +65,17 @@ class Game
     #[ORM\Column(nullable: true)]
     private ?\DateTime $validDatetime = null;
 
+    /**
+     * @var Collection<int, Gamemaster>
+     */
+    #[ORM\ManyToMany(targetEntity: Gamemaster::class, mappedBy: 'gamesCanMaster')]
+    private Collection $gamemasters;
+
     public function __construct()
     {
         $this->genre = new ArrayCollection();
         $this->id = Uuid::v7();
+        $this->gamemasters = new ArrayCollection();
     }
 
     public function getId(): ?Uuid
@@ -262,4 +269,32 @@ class Game
 
         return $this;
     }
+
+    /**
+     * @return Collection<int, Gamemaster>
+     */
+    public function getGamemasters(): Collection
+    {
+        return $this->gamemasters;
+    }
+
+    public function addGamemaster(Gamemaster $gamemaster): static
+    {
+        if (!$this->gamemasters->contains($gamemaster)) {
+            $this->gamemasters->add($gamemaster);
+            $gamemaster->addGamesCanMaster($this);
+        }
+
+        return $this;
+    }
+
+    public function removeGamemaster(Gamemaster $gamemaster): static
+    {
+        if ($this->gamemasters->removeElement($gamemaster)) {
+            $gamemaster->removeGamesCanMaster($this);
+        }
+
+        return $this;
+    }
+
 }

+ 228 - 0
src/Entity/Gamemaster.php

@@ -0,0 +1,228 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\GamemasterRepository;
+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;
+use Misd\PhoneNumberBundle\Validator\Constraints as MisdAssert;
+use Symfony\Component\Validator\Constraints as Assert;
+
+#[ORM\Entity(repositoryClass: GamemasterRepository::class)]
+#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
+#[UniqueEntity(fields: ['email'], message: 'There is already an gamemaster with this email')]
+class Gamemaster
+{
+    #[ORM\Id]
+    #[ORM\Column(type: UuidType::NAME, unique: true)]
+    #[ORM\GeneratedValue(strategy: 'CUSTOM')]
+    #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
+    private ?Uuid $id = null;
+
+    #[Assert\Email]
+    #[ORM\Column(length: 180)]
+    private ?string $email = null;   
+
+    #[ORM\Column(length: 180, nullable: true)]
+    private ?string $firstName = null;
+
+    #[ORM\Column(length: 180, nullable: true)]
+    private ?string $lastName = null;
+
+    #[ORM\Column(length: 180, nullable: true)]
+    private ?string $preferedName = null;
+
+    #[ORM\Column(type: Types::TEXT, nullable: true)]
+    private ?string $description = null;
+
+    #[MisdAssert\PhoneNumber()]
+    #[ORM\Column(length: 255, nullable: true)]
+    private ?string $phone = null;
+
+    /**
+     * @var Collection<int, Game>
+     */
+    #[ORM\ManyToMany(targetEntity: Game::class, inversedBy: 'gamemasters')]
+    #[JoinTable(name: 'mastery_gamemaster_game')]
+    private Collection $gamesCanMaster;
+
+    #[ORM\OneToOne(inversedBy: 'linkToGamemaster')]
+    private ?User $linkToUser = null;
+
+    #[ORM\Column(length: 255, nullable: true)]
+    private ?string $picture = null;
+
+    #[ORM\Column(length: 255, nullable: true)]
+    private ?string $slug = null;
+
+    public function __construct()
+    {
+        $this->gamesCanMaster = new ArrayCollection();
+    }
+
+    public function getId(): ?Uuid
+    {
+        return $this->id;
+    }
+
+    public function setId(Uuid $id): static
+    {
+        $this->id = $id;
+
+        return $this;
+    }
+
+    public function getEmail(): ?string
+    {
+        return $this->email;
+    }
+
+    public function setEmail(string $email): static
+    {
+        $this->email = $email;
+
+        return $this;
+    }
+    /**
+     * Getter / Setter pour prénom
+     */
+    public function getFirstName(): ?string
+    {
+        return $this->firstName;
+    }
+
+    public function setFirstName(string $firstName): static
+    {
+        $this->firstName = $firstName;
+
+        return $this;
+    }
+
+    /**
+     * Getter / Setter pour nom
+     */
+    public function getLastName(): ?string
+    {
+        return $this->lastName;
+    }
+
+    public function setLastName(string $lastName): static
+    {
+        $this->lastName = $lastName;
+
+        return $this;
+    }
+
+    public function getPreferedName(): ?string
+    {
+        return $this->preferedName;
+    }
+
+    public function setPreferedName(string $preferedName): static
+    {
+        $this->preferedName = $preferedName;
+
+        return $this;
+    }
+
+    public function getDescription(): ?string
+    {
+        return $this->description;
+    }
+
+    public function setDescription(?string $description): static
+    {
+        $this->description = $description;
+
+        return $this;
+    }
+
+    public function getFullName(): ?string
+    {
+        $fullName = $this->firstName . " " . $this->lastName; 
+        return $fullName;
+    }
+
+    /**
+     * Getter / Setter pour numéro de téléphone
+     */
+    public function getPhone(): ?string
+    {
+        return $this->phone;
+    }
+
+    public function setPhone(string $phone): static
+    {
+        $this->phone = $phone;
+
+        return $this;
+    }
+
+
+    /**
+     * @return Collection<int, Game>
+     */
+    public function getGamesCanMaster(): Collection
+    {
+        return $this->gamesCanMaster;
+    }
+
+    public function addGamesCanMaster(Game $gamesCanMaster): static
+    {
+        if (!$this->gamesCanMaster->contains($gamesCanMaster)) {
+            $this->gamesCanMaster->add($gamesCanMaster);
+        }
+
+        return $this;
+    }
+
+    public function removeGamesCanMaster(Game $gamesCanMaster): static
+    {
+        $this->gamesCanMaster->removeElement($gamesCanMaster);
+
+        return $this;
+    }
+
+    public function getLinkToUser(): ?User
+    {
+        return $this->linkToUser;
+    }
+
+    public function setLinkToUser(?User $linkToUser): static
+    {
+        $this->linkToUser = $linkToUser;
+
+        return $this;
+    }
+
+    public function getPicture(): ?string
+    {
+        return $this->picture;
+    }
+
+    public function setPicture(?string $picture): static
+    {
+        $this->picture = $picture;
+
+        return $this;
+    }
+
+    public function getSlug(): ?string
+    {
+        return $this->slug;
+    }
+
+    public function setSlug(?string $slug): static
+    {
+        $this->slug = $slug;
+
+        return $this;
+    }
+
+
+}

+ 26 - 1
src/Entity/User.php

@@ -70,6 +70,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
     #[ORM\OneToMany(targetEntity: Game::class, mappedBy: 'addBy')]
     private Collection $games;
 
+    #[ORM\OneToOne(mappedBy: 'linkToUser')]
+    private ?Gamemaster $linkToGamemaster = null;
+
     public function __construct()
     {
         $this->lastUpdate = new \DateTime('now');
@@ -194,7 +197,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
         return $this->phone;
     }
 
-    public function setPhone(string $phone): static
+    public function setPhone(?string $phone): static
     {
         $this->phone = $phone;
         $this->lastUpdate = new \DateTime('now');
@@ -315,4 +318,26 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
 
         return $this;
     }
+
+    public function getLinkToGamemaster(): ?Gamemaster
+    {
+        return $this->linkToGamemaster;
+    }
+
+    public function setLinkToGamemaster(?Gamemaster $linkToGamemaster): static
+    {
+        // unset the owning side of the relation if necessary
+        if ($linkToGamemaster === null && $this->linkToGamemaster !== null) {
+            $this->linkToGamemaster->setLinkToUser(null);
+        }
+
+        // set the owning side of the relation if necessary
+        if ($linkToGamemaster !== null && $linkToGamemaster->getLinkToUser() !== $this) {
+            $linkToGamemaster->setLinkToUser($this);
+        }
+
+        $this->linkToGamemaster = $linkToGamemaster;
+
+        return $this;
+    }
 }

+ 12 - 4
src/Form/ChangePasswordFormType.php

@@ -27,7 +27,7 @@ class ChangePasswordFormType extends AbstractType
                 'first_options' => [
                     'constraints' => [
                         new NotBlank([
-                            'message' => 'Please enter a password',
+                            'message' => 'Merci d\'entrer un mot de passe',
                         ]),
                         new Length([
                             'min' => 6,
@@ -39,12 +39,20 @@ class ChangePasswordFormType extends AbstractType
 //                        new PasswordStrength(),
 //                        new NotCompromisedPassword(),
                     ],
-                    'label' => 'New password',
+                    'label' => 'Mot de passe',
+                    'label_attr' => ['class' => 'label'],
+                    'attr' => ['class' => 'input'],
+                    'row_attr' => ['class' => 'field'],
+                    'help_attr' => ['class' => 'help'],
                 ],
                 'second_options' => [
-                    'label' => 'Repeat Password',
+                    'label' => 'Confirmation du mot de passe',
+                    'label_attr' => ['class' => 'label'],
+                    'attr' => ['class' => 'input'],
+                    'row_attr' => ['class' => 'field'],
+                    'help_attr' => ['class' => 'help'],
                 ],
-                'invalid_message' => 'The password fields must match.',
+                'invalid_message' => 'Les mots de passe ne correspondent pas.',
                 // Instead of being set onto the object directly,
                 // this is read and encoded in the controller
                 'mapped' => false,

+ 0 - 2
src/Form/GameType.php

@@ -20,7 +20,6 @@ class GameType extends AbstractType
         $builder
             ->add('name', null, [
                 'label' => 'Nom du jeu',
-                'empty_data' => 'Donjons & Dragons',
                 'required' => true,
                 'help' => 'Entrez le nom du jeu.',
                 'label_attr' => ['class' => 'label'],
@@ -30,7 +29,6 @@ class GameType extends AbstractType
             ])
             ->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'],

+ 126 - 0
src/Form/GamemasterType.php

@@ -0,0 +1,126 @@
+<?php
+
+namespace App\Form;
+
+use App\Entity\Game;
+use App\Entity\Gamemaster;
+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;
+
+class GamemasterType extends AbstractType
+{
+    public function buildForm(FormBuilderInterface $builder, array $options): void
+    {
+        $builder
+            ->add('firstName', null, [
+                'label' => 'Prénom',
+                'required' => true,
+                'help' => 'Entrez le prénom.',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('lastName', null, [
+                'label' => 'Nom',
+                'required' => true,
+                'help' => 'Entrez le nom.',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('preferedName', null, [
+                'label' => 'Pseudonyme',
+                'required' => true,
+                'help' => 'Entrez le pseudonyme, laissez vide pour utiliser le prénom + nom.',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('slug', null, [
+                'label' => 'Slug',
+                '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, carrée, sera découpée et redimensionnée en 250×250',
+                'required' => false,
+                'mapped' => false,
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('email', null, [
+                'label' => 'email',
+                'required' => true,
+                'help' => 'Entrez une adresse email.',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('phone', null, [
+                'label' => 'Numéro de téléphone mobile',
+                'required' => false,
+                '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('gamesCanMaster', EntityType::class, [
+                'label' => 'Jeux connus',
+                'attr' => ['class' => 'checkboxes'],
+                'class' => Game::class,
+                'choice_label' => 'name',
+                'required' => false,
+                'expanded' => true,
+                'multiple' => true,
+                'label_attr' => ['class' => 'label'],
+                'choice_attr' => ['class' => 'checkbox'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('linkToUser', EntityType::class, [
+                'label' => 'Lier à un compte utilisateur.rice',
+                'class' => User::class,
+                'choice_label' => 'email',
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'required' => false,
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+                'help' => 'Sélectionner le compte à lier à ce MJ.'
+            ])
+        ;
+    }
+
+    public function configureOptions(OptionsResolver $resolver): void
+    {
+        $resolver->setDefaults([
+            'data_class' => Gamemaster::class,
+        ]);
+    }
+}

+ 36 - 12
src/Form/RegistrationFormType.php

@@ -21,35 +21,52 @@ class RegistrationFormType extends AbstractType
             ->add('firstName', null, [
                 'label' => 'Prénom',
                 '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',
                 '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',
                 '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('phone', null, [
-                'label' => 'Téléphone',
+                'label' => 'Numéro de téléphone mobile',
                 '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('plainPassword', RepeatedType::class, [
                 'mapped' => false,
+                'required' => false,
                 'invalid_message' => 'Les mots de passe ne sont pas identiques.',
                 'type' => PasswordType::class,
                 'first_options' => [
                     'label' => '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'],
                     'constraints' => [
-                        new NotBlank([
-                            'message' => 'Merci d\'entrer un mot de passe.',
-                        ]),
                         new Length([
                             'min' => 6,
                             'minMessage' => 'Votre mot de passe doit contenir au moins {{ limit }} caractères',
@@ -58,9 +75,12 @@ class RegistrationFormType extends AbstractType
                         ]),
                     ]],
                 'second_options' => [
-                    'label' => 'Répéter le mot de passe',
+                    '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('agreeTerms', CheckboxType::class, [
@@ -71,6 +91,10 @@ class RegistrationFormType extends AbstractType
                         'message' => 'Vous devez accepter les conditions d\'utilisation du service.',
                     ]),
                 ],
+                'label_attr' => ['class' => 'checkbox'],
+                'attr' => ['class' => 'checkbox'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
             ])
 /*             ->add('plainPassword', PasswordType::class, [
                 // instead of being set onto the object directly,

+ 7 - 1
src/Form/ResetPasswordRequestFormType.php

@@ -14,13 +14,19 @@ class ResetPasswordRequestFormType extends AbstractType
     {
         $builder
             ->add('email', EmailType::class, [
-                'label' => 'eMail',
+                'label' => 'Email',
                 'attr' => ['autocomplete' => 'email'],
                 'constraints' => [
                     new NotBlank([
                         'message' => 'Entrez votre adresse de courriel',
                     ]),
                 ],
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+                'help' => 'Entrez votre adresse de courriel pour recevoir un lien de réinitialisation de votre mot de passe.
+                Si vous ne recevez pas de courriel, vérifiez votre dossier de spam ou contactez nous.',
             ])
         ;
     }

+ 0 - 4
src/Form/UserType.php

@@ -18,7 +18,6 @@ class UserType extends AbstractType
         $builder
             ->add('firstName', null, [
                 'label' => 'Prénom',
-                'empty_data' => 'Anne',
                 'required' => true,
                 'help' => 'Entrez le prénom de l\'utilisateur.',
                 'label_attr' => ['class' => 'label'],
@@ -28,7 +27,6 @@ class UserType extends AbstractType
             ])
             ->add('lastName', null, [
                 'label' => 'Nom',
-                'empty_data' => 'Nonyme',
                 'required' => true,
                 'help' => 'Entrez le nom de l\'utilisateur.',
                 'label_attr' => ['class' => 'label'],
@@ -38,7 +36,6 @@ class UserType extends AbstractType
             ])
             ->add('email', null, [
                 'label' => 'email',
-                'empty_data' => 'a.nonyme@courriel.fr',
                 'required' => true,
                 'help' => 'Entrez une adresse email.',
                 'label_attr' => ['class' => 'label'],
@@ -56,7 +53,6 @@ class UserType extends AbstractType
             ])
             ->add('phone', null, [
                 '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 mobile (optionnel).',
                 'label_attr' => ['class' => 'label'],

+ 61 - 0
src/Repository/GamemasterRepository.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\Gamemaster;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<Gamemaster>
+ */
+class GamemasterRepository extends ServiceEntityRepository
+{
+    public function __construct(ManagerRegistry $registry)
+    {
+        parent::__construct($registry, Gamemaster::class);
+    }
+
+    public function findByEmail(string $email): ?Gamemaster
+    {
+        return $this->createQueryBuilder('g')
+            ->where('g.email = :email')
+            ->setParameter('email', $email)
+            ->getQuery()
+            ->getOneOrNullResult()
+            ;    
+    }
+
+    public function findUnlinked(): array
+    {
+        return $this->createQueryBuilder('g')
+            ->where('g.linkToUser IS null')
+            ->getQuery()
+            ->getResult();
+    }
+
+    //    /**
+    //     * @return Gamemaster[] Returns an array of Gamemaster 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): ?Gamemaster
+    //    {
+    //        return $this->createQueryBuilder('g')
+    //            ->andWhere('g.exampleField = :val')
+    //            ->setParameter('val', $value)
+    //            ->getQuery()
+    //            ->getOneOrNullResult()
+    //        ;
+    //    }
+}

+ 10 - 0
src/Repository/UserRepository.php

@@ -48,6 +48,16 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
         return $qb->getResult();
     }
 
+    public function findByEmail(string $email): ?User
+    {
+        return $this->createQueryBuilder('u')
+            ->where('u.email = :email')
+            ->setParameter('email', $email)
+            ->getQuery()
+            ->getOneOrNullResult()
+            ;    
+    }
+
     //    /**
     //     * @return User[] Returns an array of User objects
     //     */

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

@@ -36,7 +36,7 @@
             {% 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>{% if game.isValidByAdmin %}<span class="icon"><twig:ux:icon name="bi:check"/></span>{% else %}<a href="{{ path('app_admin_game_valid', {id: game.id}) }}" class="button">Valider</a>{%endif%}</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>

+ 83 - 0
templates/admin/gamemaster/edit.html.twig

@@ -0,0 +1,83 @@
+{% extends 'bulma.html.twig' %}
+
+{% block title %}Administration > Meneur(euse) de jeux > Éditer{% endblock %}
+
+{% block content %}
+
+    {{ form_errors(form) }}
+    {{ form_start(form) }}
+
+    {{ form_row(form.firstName) }}
+    {{ form_row(form.lastName) }}
+    {{ form_row(form.preferedName) }}
+    {{ form_row(form.slug) }}
+    {{ form_row(form.description)}}
+
+    <div class="box">
+        {{ form_row(form.email) }}
+        {{ form_row(form.phone) }}
+    </div>
+
+    {# 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 gamemaster.picture %}
+                <div class="field mt-2">
+                <p><a href="{{ path('app_admin_gamemaster_del_pic', {id: gamemaster.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 gamemaster.picture %}
+                <img src="/images/gamemasters/{{ gamemaster.picture }}" class="image is-3by1"/>
+                {% endif %}
+            </div>
+        </div>
+    </div>
+    </div>
+
+    <div class="box">
+    <div class="field">
+        {{ form_label(form.gamesCanMaster) }}
+        {{ form_widget(form.gamesCanMaster) }}
+        {{ form_help(form.gamesCanMaster) }}
+    </div>
+    </div>
+
+    <div class="box">
+        {{ form_row(form.linkToUser) }}
+    </div>
+
+
+    {{ form_widget(form) }}
+    
+
+
+
+
+    <div class="control">
+        <button class="button is-primary" type="submit">Envoyer</button>
+        <a href="{{ path('app_admin_gm') }}" class="button">Annuler</a>
+    </div>
+
+    {{ form_end(form) }}
+
+{% endblock %}

+ 55 - 0
templates/admin/gamemaster/index.html.twig

@@ -0,0 +1,55 @@
+{% extends 'bulma.html.twig' %}
+
+{% block title %}Administration > Utilisateurs{% 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_gm') }}">Gestion des meneur(euse)s de jeu</a></li>
+      </ul>
+    </nav>
+
+    <div class="block">
+        <h1 class="title">Gestion des meneur(euse)s de jeu</h1>
+        <p class="subtitle">Liste des MJ enregistré(e)s dans l'application.</p>
+    </div>
+
+    <div class="block">
+        <div class="is-grouped">
+            <a class="button is-primary" href="{{ path('app_admin_gamemaster_add') }}">Ajouter un(e) meneur(euse) de jeu</a>
+            <a class="button" href="{{ path('app_admin_gamemaster_link') }}">Associer les MJ à leurs comptes</a>
+        </div>
+    </div>
+
+    <div class="block">
+        <table id="datatable" {{ stimulus_controller('datatables') }} class="table is-striped is-hoverable is-fullwidth">
+            <thead>
+            <tr>
+                <th>Identité</th>
+                <th>Email</th>
+                <th>Compte lié</th>
+                <th>Actions</th>
+            </tr>
+            </thead>
+            <tbody>
+            {% for gamemaster in gamemasters %}
+                <tr>
+                    <td><a href="{{ path('app_admin_gamemaster_edit', {id: gamemaster.id}) }}">{{ gamemaster.preferedName }}</a></td>
+                    <td><span class="icon-text">{{ gamemaster.email }}</span></td>
+                    <td>{% if gamemaster.getLinkToUser %}<a href="{{ path('app_admin_user_edit', {id: gamemaster.getLinkToUser.id}) }}" class="button">Éditer le compte lié{% else %}aucun{% endif %}</a></td>
+                    <td>
+                        <a class="button" href="{{ path('app_admin_gamemaster_edit', {id: gamemaster.id}) }}">Éditer</a>
+                        <a class="button is-danger" data-id="{{ path('app_admin_gamemaster_delete', {'id': gamemaster.id})}}" href="#" {{ stimulus_controller('admin_confirm') }}>Supprimer</a>
+                    </td>
+                </tr>
+            {% endfor %}
+            </tbody>
+
+        </table>
+    </div>
+
+
+{% endblock %}

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

@@ -65,7 +65,7 @@
           <div class="box">
             <h2 class="title is-4">Meneurs et meneuses de jeux</h2>
             <p>Gérer les meneurs et meneuses de jeux.</p>
-            <p><a href="#">Gérer les MJ</a></p>
+            <p><a href="{{ path('app_admin_gm') }}">Gérer les MJ</a></p>
           </div>
         </div>
       </div>

+ 2 - 0
templates/admin/user/index.html.twig

@@ -30,6 +30,7 @@
                 <th>Identité</th>
                 <th>Email</th>
                 <th>Rôles</th>
+                <th>MJ?</th>
                 <th>Dernière connexion</th>
                 <th>Dernière mise à jour</th>
                 <th>Actions</th>
@@ -41,6 +42,7 @@
                     <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>{% if user.getLinkToGamemaster %}<a href="{{ path('app_admin_gamemaster_edit', {id: user.getLinkToGamemaster.id}) }}" class="button">Éditer le profil MJ lié</a>{% else %}<a href="{{ path('app_admin_user_create_gm', {id: user.id}) }}" class="button">Créer un profil MJ</a>{% endif %}</td>
                     <td>{{ user.lastLogin ? user.lastLogin|date('d/m/Y H:i:s', app_timezone) : 'Jamais' }}</td>
                     <td>{{ user.lastUpdate ? user.lastUpdate|date('d/m/Y H:i:s', app_timezone) : 'Jamais' }}</td>
                     <td>

+ 2 - 0
templates/bulma.html.twig

@@ -53,9 +53,11 @@
                 <div class="navbar-dropdown is-right">
                   <a class="navbar-item" href="#" class="">Mon compte</a>
                   <a class="navbar-item" href="#">Mes réservations</a>
+                  {% if is_granted('ROLE_ADMIN') %}
                   <hr class="navbar-divider" />
                   <a class="navbar-item" href="{{ path('app_admin') }}">Administration</a>
                   <hr class="navbar-divider" />
+                  {% endif %}
                   <a class="navbar-item" href="{{ path('app_logout') }}">Me déconnecter</a>
                 </div>
               </div>

+ 0 - 2
templates/main/markdown.html.twig

@@ -4,8 +4,6 @@
 
 {% block content %}
 
-{{ component('Alert', {title: '', message: 'Cette page est en cours de rédaction.', type: 'info'}) }}
-
 <div class="content">
 {{ codeContent|markdown_to_html }}
 </div>

+ 33 - 10
templates/registration/register.html.twig

@@ -1,19 +1,42 @@
-{% extends 'base.html.twig' %}
+{% extends 'bulma.html.twig' %}
 
 {% block title %}S'enregistrer{% endblock %}
 
 {% block content %}
-    {% for flash_error in app.flashes('verify_email_error') %}
-        <div class="alert alert-danger" role="alert">{{ flash_error }}</div>
-    {% endfor %}
+    <div class="content">
+        <div class="container">
+            <div class="columns is-centered">
+                <div class="column is-5">
+                    <div class="box">
+                    {% for flash_error in app.flashes('verify_email_error') %}
+                        <div class="alert alert-danger" role="alert">{{ flash_error }}</div>
+                    {% endfor %}
 
-    <h1>S'enregistrer</h1>
+                    <h1 class="title is-4 has-text-centered">Créer un compte utilisateur</h1>
+                    {{ form_errors(registrationForm) }}
 
-    {{ form_errors(registrationForm) }}
+                    {{ form_start(registrationForm) }}
 
-    {{ form_start(registrationForm) }}
+                    {{ form_row(registrationForm.firstName) }}
+                    {{ form_row(registrationForm.lastName) }}
+                    {{ form_row(registrationForm.email) }}
+                    {{ form_row(registrationForm.phone) }}
 
-        <button type="submit" class="btn">S'enregistrer</button>
-        <a href="{{ path('app_main') }}" class="btn btn-secondary">Annuler</a>
-    {{ form_end(registrationForm) }}
+                    {{ form_row(registrationForm.plainPassword)}}
+
+                    <div class="field">
+                        <label>
+                        {{ form_widget(registrationForm.agreeTerms) }}
+                         J'accepte les <a href="{{ path('app_terms') }}" target="_blank">conditions générales d'utilisation du service</a>.
+                        </label>
+                    </div>
+                    
+                    <button type="submit" class="button is-primary">S'enregistrer</button>
+                    <a href="{{ path('app_main') }}" class="button">Annuler</a>
+                    {{ form_end(registrationForm) }}
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
 {% endblock %}

+ 16 - 5
templates/reset_password/check_email.html.twig

@@ -1,11 +1,22 @@
-{% extends 'base.html.twig' %}
+{% extends 'bulma.html.twig' %}
 
-{% block title %}Password Reset Email Sent{% endblock %}
+{% block title %}Courriel envoyé{% endblock %}
 
 {% block content %}
+<div class="container">
+  <div class="columns is-centered">
+    <div class="column is-5">
+      <div class="message is-primary">
+        <div class="message-header">Email envoyé !</div>
+        <div class="message-body">
     <p>
-        If an account matching your email exists, then an email was just sent that contains a link that you can use to reset your password.
-        This link will expire in {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.
+        Si un compte correspond à cette adresse de courriel, vous recevrez un email contenant un lien vous permettant de réinitialiser votre mot de passe.
+        Ce lien expirera dans {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.
     </p>
-    <p>If you don't receive an email please check your spam folder or <a href="{{ path('app_forgot_password_request') }}">try again</a>.</p>
+    <p>Si bous n'avez pas reçu cet email, consultez vos SPAM ou <a href="{{ path('app_forgot_password_request') }}">essayez à nouveau la procédure.</a>.</p>
+    </div></div>
+    </div>
+    </div>
+    </div>
+
 {% endblock %}

+ 24 - 6
templates/reset_password/email.html.twig

@@ -1,9 +1,27 @@
-<h1>Hi!</h1>
+{% extends 'base.email.html.twig' %}
 
-<p>To reset your password, please visit the following link</p>
+{% block title %}Réinitialisation de votre mot de passe{% endblock %}
 
-<a href="{{ url('app_reset_password', {token: resetToken.token}) }}">{{ url('app_reset_password', {token: resetToken.token}) }}</a>
+{% block content %}
+                    <tr>
+                        <td style="padding: 0 20px 10px 20px; font-size: 16px;">
+                            <p style="margin: 0;">Bonjour,</p>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td style="padding: 0 20px 20px 20px; font-size: 16px;">
+                            <p style="margin: 0;">Voici le lien pour réinitialiser votre mot de passe :</p>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td style="padding: 0 20px 20px 20px;">
+                            <a href="{{ url('app_reset_password', {token: resetToken.token}) }}">{{ url('app_reset_password', {token: resetToken.token}) }}</a>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td style="padding: 0 20px 20px 20px;">
+                            <p style="margin: 0;">Ce lien expirera {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.</p>
+                        </td>
+                    </tr>
 
-<p>This link will expire in {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.</p>
-
-<p>Cheers!</p>
+{% endblock %}

+ 6 - 0
templates/reset_password/email.txt.twig

@@ -0,0 +1,6 @@
+Bonjour,
+
+Voici le lien pour réinitialiser votre mot de passe :
+{{ url('app_reset_password', {token: resetToken.token}) }}
+
+Ce lien expirera {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.

+ 15 - 9
templates/reset_password/request.html.twig

@@ -1,22 +1,28 @@
-{% extends 'base.html.twig' %}
+{% extends 'bulma.html.twig' %}
 
 {% block title %}Réinitialisez votre mot de passe{% endblock %}
 
 {% block content %}
+    <div class="content">
+        <div class="container">
+            <div class="columns is-centered">
+                <div class="column is-5">
+                    <div class="box">
     {% for flash_error in app.flashes('reset_password_error') %}
         <div class="alert alert-danger" role="alert">{{ flash_error }}</div>
     {% endfor %}
-    <h1>Réinitialisez votre mot de passe</h1>
+    <h1 class="title is-4 has-text-centered">Réinitialisez votre mot de passe</h1>
 
     {{ form_start(requestForm) }}
         {{ form_row(requestForm.email) }}
-        <div>
-            <small>
-                Entrez votre adresse de courriel pour recevoir un lien de réinitialisation de votre mot de passe.
-                Si vous ne recevez pas de courriel, vérifiez votre dossier de spam ou contactez
-            </small>
-        </div>
 
-        <button class="btn btn-primary">Envoyer un courriel de réinitialisation</button>
+
+        <button class="button is-primary">Envoyer un courriel de réinitialisation</button>
     {{ form_end(requestForm) }}
+    </div>
+    </div>
+    </div>
+    </div>
+    </div>
+
 {% endblock %}

+ 1 - 1
templates/reset_password/reset.html.twig

@@ -13,7 +13,7 @@
           <h1 class="title is-4 has-text-centered">Réinitialisez votre mot de passe</h1>
 
         {{ form_row(resetForm.plainPassword) }}
-        <button class="btn btn-primary">Reset password</button>
+        <button class="button is-primary">Reset password</button>
     {{ form_end(resetForm) }}
 </div>
 </div>