Browse Source

gestion des profils utilisateurs par l'utilisateur lui-même

garthh 1 day ago
parent
commit
a3eff59ac3

+ 31 - 0
migrations/Version20250731051535.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 Version20250731051535 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 event (id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', name VARCHAR(255) NOT NULL, description LONGTEXT DEFAULT NULL, picture VARCHAR(255) DEFAULT NULL, start_on DATETIME NOT NULL COMMENT \'(DC2Type:datetimetz_immutable)\', end_on DATETIME NOT NULL COMMENT \'(DC2Type:datetimetz_immutable)\', more_link VARCHAR(255) DEFAULT NULL, slug VARCHAR(255) DEFAULT NULL, published TINYINT(1) DEFAULT NULL, public TINYINT(1) DEFAULT NULL, everyone_can_show_planning TINYINT(1) DEFAULT NULL, everyone_can_ask_for_game TINYINT(1) 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 event');
+    }
+}

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

@@ -49,6 +49,10 @@ 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
     {
+        if (!$user) {
+            $this->addFlash('danger', 'Aucun utilisateur sélectionné.');
+            return $this->redirectToRoute('app_admin_user');
+        }
         // Si MJ, supprimer le lien avant de supprimer
         if ($user->getLinkToGamemaster()) {
             $user->setLinkToGamemaster(null);
@@ -67,6 +71,10 @@ final class UserController extends AbstractController
     #[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 (!$user) {
+            $this->addFlash('danger', 'Aucun utilisateur sélectionné.');
+            return $this->redirectToRoute('app_admin_user');
+        }
         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');

+ 183 - 7
src/Controller/ProfileController.php

@@ -3,24 +3,200 @@
 namespace App\Controller;
 
 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\PasswordHasher\Hasher\UserPasswordHasherInterface;
+use Symfony\Component\String\Slugger\AsciiSlugger;
+use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
+use App\Form\UserProfileType;
+use App\Form\GamemasterProfileType;
+use App\Form\GameProfileType;
+use App\Repository\GameRepository;
+use App\Entity\User;
+use App\Entity\Gamemaster;
+use App\Entity\Game;
+use App\Service\PictureService;
+
 
 final class ProfileController extends AbstractController
 {
-    #[Route('/profile', name: 'app_profile')]
-    public function index(): Response
+    #[Route('/profile', name: 'app_profile', methods: ['GET', 'POST'])]
+    public function index(Request $request, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $manager): Response
     {
+        $user = $this->getUser();
+        $form = $this->createForm(UserProfileType::class, $user);
+
+        // Mise à jour de l'utilisateur à partir du formulaire
+        $form->handleRequest($request);
+        if ($form->isSubmitted() && $form->isValid()) {
+            // Si un nouveau mot de passe a été proposé
+            if ($form->get('newPassword')->getData()) {
+                // Encoder le mot de passe
+                $newPassword = $form->get('newPassword')->getData();
+                $user->setPassword($userPasswordHasher->hashPassword($user, $newPassword));
+            }
+            if ($user->getLinkToGamemaster()) {
+                $user->getLinkToGamemaster()->setPhone($user->getPhone());
+                $user->getLinkToGamemaster()->setFirstName($user->getFirstName());
+                $user->getLinkToGamemaster()->setLastName($user->getLastName());
+            }
+            // Mettre à jour l'utilisateur
+            $user->setLastUpdate();
+            $manager->persist($user);
+            $manager->flush();
+            $this->addFlash('success', 'Compte modifié avec succès.');
+            return $this->redirectToRoute('app_profile');
+        }
+
         return $this->render('profile/index.html.twig', [
-            'controller_name' => 'ProfileController',
+            'form' => $form,
+            'user' => $user,
         ]);
     }
 
-    #[Route('/profile/reservation', name: 'app_profile_reservations')]
-    public function reservations(): Response
+    #[Route('/profile/gamemaster', name: 'app_profile_gamemaster', methods: ['GET', 'POST'])]
+    public function gamemasterProfile(Request $request, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $manager, PictureService $pictureService): Response
     {
-        return $this->render('profile/index.html.twig', [
-            'controller_name' => 'ProfileController'
+        $user = $this->getUser();
+        $gamemaster = $user->getLinkToGamemaster();
+        if (!$gamemaster) {
+            $this->addFlash('error', 'Vous n\'avez pas de profil de meneur(euse) de jeu.');
+            return $this->redirectToRoute('app_profile');
+        }
+        $form = $this->createForm(GamemasterProfileType::class, $gamemaster);
+
+        // Mise à jour de l'utilisateur à partir du formulaire
+        $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);
+            }
+
+            // Mettre à jour l'utilisateur
+            $manager->persist($gamemaster);
+            $manager->flush();
+            $this->addFlash('success', 'Profil MJ modifié avec succès.');
+            return $this->redirectToRoute('app_profile_gamemaster');
+        }
+
+        return $this->render('profile/gamemaster.html.twig', [
+            'form' => $form,
+            'gamemaster' => $gamemaster,
+        ]);
+    }
+
+    /*
+     * Supprimer l'image du MJ
+     */
+    #[Route('/profile/gamemaster/{id}/del-pic', name: 'app_profile_gamemaster_del_pic', requirements: ['id' => Requirement::UUID_V7], methods: ['GET'])]
+    public function deletePicture(?Gamemaster $gamemaster, EntityManagerInterface $manager, ParameterBagInterface $params): Response
+    {
+        $user = $this->getUser();
+        $gamemaster = $user->getLinkToGamemaster();
+        if (!$gamemaster) {
+            $this->addFlash('error', 'Vous n\'avez pas de profil de meneur(euse) de jeu.');
+            return $this->redirectToRoute('app_profile');
+        }
+        // Effacer toutes images associée
+        if ($gamemaster->getPicture()) {
+            $fullPath = $params->get('upload_images_directory') . '/gamemasters/' . $gamemaster->getPicture();
+            unlink($fullPath);
+        }
+        $gamemaster->setPicture(null);
+        $manager->flush();
+        
+        $this->addFlash('success', 'Illustration supprimée avec succès.');
+        return $this->redirectToRoute('app_profile_gamemaster');
+    }
+
+    #[Route('/profile/gamelist', name: 'app_profile_gamelist')]
+    public function gameList(GameRepository $repository): Response
+    {
+        $user = $this->getUser();
+        $gamemaster = $user->getLinkToGamemaster();
+        if (!$gamemaster) {
+            $this->addFlash('error', 'Vous n\'avez pas de profil de meneur(euse) de jeu.');
+            return $this->redirectToRoute('app_profile');
+        }
+        $games = $repository->findAllValid();
+        return $this->render('profile/gamelist.html.twig', [
+            'games' => $games,
+        ]);
+    }
+
+    #[Route('/profile/gameadd', name: 'app_profile_gameadd', methods: ['GET', 'POST'])]
+    public function gameAdd(Request $request, EntityManagerInterface $manager, PictureService $pictureService): Response
+    {
+        $user = $this->getUser();
+        $gamemaster = $user->getLinkToGamemaster();
+        if (!$gamemaster) {
+            $this->addFlash('error', 'Vous n\'avez pas de profil de meneur(euse) de jeu.');
+            return $this->redirectToRoute('app_profile');
+        }
+        // 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(GameProfileType::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. Il sera prochainement validé par l\'équipe d\'administration.');
+            return $this->redirectToRoute('app_profile_gamelist');
+        }
+
+        return $this->render('profile/gameadd.html.twig', [
+            'form' => $form,
+            'game' => $game
+        ]);
+    }
+
+
+
+    #[Route('/profile/games', name: 'app_profile_games')]
+    public function games(): Response
+    {
+        return $this->render('profile/games.html.twig', [
         ]);
     }
 }

+ 190 - 0
src/Entity/Event.php

@@ -0,0 +1,190 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\EventRepository;
+use Doctrine\DBAL\Types\Types;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Bridge\Doctrine\Types\UuidType;
+use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
+use Symfony\Component\Uid\Uuid;
+
+#[ORM\Entity(repositoryClass: EventRepository::class)]
+class Event
+{
+    #[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(type: Types::TEXT, nullable: true)]
+    private ?string $description = null;
+
+    #[ORM\Column(length: 255, nullable: true)]
+    private ?string $picture = null;
+
+    #[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE)]
+    private ?\DateTimeImmutable $startOn = null;
+
+    #[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE)]
+    private ?\DateTimeImmutable $endOn = null;
+
+    #[ORM\Column(type: Types::ASCII_STRING, nullable: true)]
+    private $moreLink;
+
+    #[ORM\Column(type: Types::ASCII_STRING, nullable: true)]
+    private $slug;
+
+    #[ORM\Column(nullable: true)]
+    private ?bool $published = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?bool $public = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?bool $everyoneCanShowPlanning = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?bool $everyoneCanAskForGame = null;
+
+    public function getId(): ?Uuid
+    {
+        return $this->id;
+    }
+
+    public function getName(): ?string
+    {
+        return $this->name;
+    }
+
+    public function setName(string $name): static
+    {
+        $this->name = $name;
+
+        return $this;
+    }
+
+    public function getDescription(): ?string
+    {
+        return $this->description;
+    }
+
+    public function setDescription(?string $description): static
+    {
+        $this->description = $description;
+
+        return $this;
+    }
+
+    public function getPicture(): ?string
+    {
+        return $this->picture;
+    }
+
+    public function setPicture(?string $picture): static
+    {
+        $this->picture = $picture;
+
+        return $this;
+    }
+
+    public function getStartOn(): ?\DateTimeImmutable
+    {
+        return $this->startOn;
+    }
+
+    public function setStartOn(\DateTimeImmutable $startOn): static
+    {
+        $this->startOn = $startOn;
+
+        return $this;
+    }
+
+    public function getEndOn(): ?\DateTimeImmutable
+    {
+        return $this->endOn;
+    }
+
+    public function setEndOn(\DateTimeImmutable $endOn): static
+    {
+        $this->endOn = $endOn;
+
+        return $this;
+    }
+
+    public function getMoreLink()
+    {
+        return $this->moreLink;
+    }
+
+    public function setMoreLink($moreLink): static
+    {
+        $this->moreLink = $moreLink;
+
+        return $this;
+    }
+
+    public function getSlug()
+    {
+        return $this->slug;
+    }
+
+    public function setSlug($slug): static
+    {
+        $this->slug = $slug;
+
+        return $this;
+    }
+
+    public function isPublished(): ?bool
+    {
+        return $this->published;
+    }
+
+    public function setPublished(?bool $published): static
+    {
+        $this->published = $published;
+
+        return $this;
+    }
+
+    public function isPublic(): ?bool
+    {
+        return $this->public;
+    }
+
+    public function setPublic(?bool $public): static
+    {
+        $this->public = $public;
+
+        return $this;
+    }
+
+    public function isEveryoneCanShowPlanning(): ?bool
+    {
+        return $this->everyoneCanShowPlanning;
+    }
+
+    public function setEveryoneCanShowPlanning(?bool $everyoneCanShowPlanning): static
+    {
+        $this->everyoneCanShowPlanning = $everyoneCanShowPlanning;
+
+        return $this;
+    }
+
+    public function isEveryoneCanAskForGame(): ?bool
+    {
+        return $this->everyoneCanAskForGame;
+    }
+
+    public function setEveryoneCanAskForGame(?bool $everyoneCanAskForGame): static
+    {
+        $this->everyoneCanAskForGame = $everyoneCanAskForGame;
+
+        return $this;
+    }
+}

+ 80 - 0
src/Form/GameProfileType.php

@@ -0,0 +1,80 @@
+<?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 GameProfileType extends AbstractType
+{
+    public function buildForm(FormBuilderInterface $builder, array $options): void
+    {
+        $builder
+            ->add('name', null, [
+                'label' => 'Nom du jeu',
+                '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',
+                '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'],
+            ]);
+ 
+    }
+
+    public function configureOptions(OptionsResolver $resolver): void
+    {
+        $resolver->setDefaults([
+            'data_class' => Game::class,
+        ]);
+    }
+}

+ 82 - 0
src/Form/GamemasterProfileType.php

@@ -0,0 +1,82 @@
+<?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;
+use App\Repository\UserRepository;
+
+class GamemasterProfileType extends AbstractType
+{
+    public function buildForm(FormBuilderInterface $builder, array $options): void
+    {
+        $builder
+            ->add('preferedName', null, [
+                'label' => 'Pseudonyme',
+                'required' => false,
+                'help' => 'Entrez un 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 à partir du pseudonyme, 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' => 'Votre biographie.',
+                '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('gamesCanMaster', EntityType::class, [
+                'label' => 'Jeux connus',
+                'help' => 'Cochez les jeux que vous pouvez animer.',
+                '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'],
+            ])
+        ;
+    }
+
+    public function configureOptions(OptionsResolver $resolver): void
+    {
+        $resolver->setDefaults([
+            'data_class' => Gamemaster::class,
+        ]);
+    }
+}

+ 1 - 1
src/Form/GamemasterType.php

@@ -38,7 +38,7 @@ class GamemasterType extends AbstractType
             ])
             ->add('preferedName', null, [
                 'label' => 'Pseudonyme',
-                'required' => true,
+                'required' => false,
                 'help' => 'Entrez le pseudonyme, laissez vide pour utiliser le prénom + nom.',
                 'label_attr' => ['class' => 'label'],
                 'attr' => ['class' => 'input'],

+ 85 - 0
src/Form/UserProfileType.php

@@ -0,0 +1,85 @@
+<?php
+
+namespace App\Form;
+
+use App\Entity\User;
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
+use Symfony\Component\Form\Extension\Core\Type\PasswordType;
+use Symfony\Component\Validator\Constraints\Length;
+use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
+
+class UserProfileType extends AbstractType
+{
+    public function buildForm(FormBuilderInterface $builder, array $options): void
+    {
+        $builder
+            ->add('firstName', null, [
+                'label' => 'Prénom',
+                'required' => true,
+                'label_attr' => ['class' => 'label'],
+                'attr' => ['class' => 'input'],
+                'row_attr' => ['class' => 'field'],
+                'help_attr' => ['class' => 'help'],
+            ])
+            ->add('lastName', null, [
+                'label' => 'Nom',
+                'required' => true,
+                '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('newPassword', 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', 'class' => 'input'],
+                    'label_attr' => ['class' => 'label'],
+                    'row_attr' => ['class' => 'field'],
+                    'help_attr' => ['class' => 'help'],
+                    'constraints' => [
+                        new Length([
+                            'min' => 6,
+                            'minMessage' => 'Votre mot de passe doit contenir au moins {{ limit }} caractères',
+                            // max length allowed by Symfony for security reasons
+                            'max' => 4096,
+                        ]),
+                    ]],
+                'second_options' => [
+                    'label' => 'Répétez le mot de passe',
+                    'toggle' => true,
+                    'attr' => ['autocomplete' => 'new-password', 'class' => 'input'],
+                    'label_attr' => ['class' => 'label'],
+                    'row_attr' => ['class' => 'field'],
+                    'help_attr' => ['class' => 'help'],
+                    ],
+            ])
+            /*->add('lastUpdate')
+            ->add('lastLogin')
+            ->add('lastAction') */
+        ;
+    }
+
+    public function configureOptions(OptionsResolver $resolver): void
+    {
+        $resolver->setDefaults([
+            'data_class' => User::class,
+        ]);
+    }
+}

+ 64 - 0
src/Repository/EventRepository.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\Event;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<Event>
+ */
+class EventRepository extends ServiceEntityRepository
+{
+    public function __construct(ManagerRegistry $registry)
+    {
+        parent::__construct($registry, Event::class);
+    }
+
+    /*
+     * Extraire les événements :
+     * - public = true
+     * - published = true
+     * - endOn > now
+     */
+    public function findAllPublicToGo(): array
+    {
+        
+    }
+
+    /*
+     * Extraire les événements :
+     * - published = true
+     * - endOn > now
+     */
+    public function findAllPrivateToGo(): array 
+    {
+
+    }
+
+    //    /**
+    //     * @return Event[] Returns an array of Event objects
+    //     */
+    //    public function findByExampleField($value): array
+    //    {
+    //        return $this->createQueryBuilder('e')
+    //            ->andWhere('e.exampleField = :val')
+    //            ->setParameter('val', $value)
+    //            ->orderBy('e.id', 'ASC')
+    //            ->setMaxResults(10)
+    //            ->getQuery()
+    //            ->getResult()
+    //        ;
+    //    }
+
+    //    public function findOneBySomeField($value): ?Event
+    //    {
+    //        return $this->createQueryBuilder('e')
+    //            ->andWhere('e.exampleField = :val')
+    //            ->setParameter('val', $value)
+    //            ->getQuery()
+    //            ->getOneOrNullResult()
+    //        ;
+    //    }
+}

+ 9 - 0
src/Repository/GameRepository.php

@@ -16,6 +16,15 @@ class GameRepository extends ServiceEntityRepository
         parent::__construct($registry, Game::class);
     }
 
+    public function findAllValid(): array
+    {
+        return $this->createQueryBuilder('g')
+            ->where('g.isValidByAdmin = true')
+            ->orderBy('g.name')
+            ->getQuery()
+            ->getResult();
+    }
+
     //    /**
     //     * @return Genre[] Returns an array of Genre objects
     //     */

+ 1 - 1
templates/admin/gamemaster/edit.html.twig

@@ -47,7 +47,7 @@
             </div>
             <div class="column">
                 {% if gamemaster.picture %}
-                <img src="/images/gamemasters/{{ gamemaster.picture }}" class="image is-3by1"/>
+                <img src="/images/gamemasters/{{ gamemaster.picture }}" class="image is-1by1"/>
                 {% endif %}
             </div>
         </div>

+ 3 - 4
templates/admin/index.html.twig

@@ -11,11 +11,10 @@
       </ul>
     </nav>
 
-  <section class="section">
 
 
 
-    <div class="container">
+
       <div class="columns">
         <!-- Colonne 1 -->
         <div class="column">
@@ -69,7 +68,7 @@
           </div>
         </div>
       </div>
-    </div>
-  </section>
+
+
 
 {% endblock %}

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

@@ -42,7 +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>{% if user.getLinkToGamemaster %}<a href="{{ path('app_admin_gamemaster_edit', {id: user.getLinkToGamemaster.id}) }}" class="button">Éditer le profil MJ lié</a>{% else %}<a data-id="{{ path('app_admin_user_create_gm', {id: user.id}) }}" class="button" href="#" {{ stimulus_controller('admin_confirm')}}>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>

+ 1 - 1
templates/bulma.html.twig

@@ -52,7 +52,7 @@
                 <a class="navbar-link">{{ app.user.firstName }}</a>
                 <div class="navbar-dropdown is-right">
                   <a class="navbar-item" href="{{ path('app_profile') }}" class="">Mon compte</a>
-                  <a class="navbar-item" href="#">Mes réservations</a>
+                  <a class="navbar-item" href="{{ path('app_profile_games') }}">Mes parties</a>
                   {% if is_granted('ROLE_ADMIN') %}
                   <hr class="navbar-divider" />
                   <a class="navbar-item" href="{{ path('app_admin') }}">Administration</a>

+ 84 - 0
templates/profile/gameadd.html.twig

@@ -0,0 +1,84 @@
+{% extends 'bulma.html.twig' %}
+
+{% block title %}Mon Compte{% endblock %}
+
+{% block content %}
+
+    <nav class="breadcrumb has-arrow-separator" aria-label="breadcrumbs">
+      <ul>
+        <li><a href="{{ path('app_main') }}">Accueil</a></li>
+        <li class="is-active"><a href="{{ path('app_profile') }}">Mon compte</a></li>
+      </ul>
+    </nav>
+
+    <div class="tabs is-boxed">
+        <ul>
+            <li ><a href="{{ path('app_profile') }}">Compte utilisateur(rice)</a></li>
+            {% if app.user.linkToGamemaster %}
+            <li><a href="{{ path('app_profile_gamemaster') }}">Meneur(euse) de jeu</a></li>
+            <li><a href="{{ path('app_profile_gamelist') }}">Ludothèque</a></li>
+            <li class="is-active"><a>Proposer un jeu</a></li>
+            {% endif %}
+        </ul>
+    </div>   
+
+    {{ 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="control">
+        <button class="button is-primary" type="submit">Envoyer</button>
+    </div>
+
+    {{ form_end(form) }}
+
+{% endblock %}

+ 64 - 0
templates/profile/gamelist.html.twig

@@ -0,0 +1,64 @@
+{% extends 'bulma.html.twig' %}
+
+{% block title %}Mon compte{% endblock %}
+
+{% block content %}
+
+    <nav class="breadcrumb has-arrow-separator" aria-label="breadcrumbs">
+      <ul>
+        <li><a href="{{ path('app_main') }}">Accueil</a></li>
+        <li class="is-active"><a href="{{ path('app_profile') }}">Mon compte</a></li>
+      </ul>
+    </nav>
+
+    <div class="tabs is-boxed">
+        <ul>
+            <li ><a href="{{ path('app_profile') }}">Compte utilisateur(rice)</a></li>
+            {% if app.user.linkToGamemaster %}
+            <li><a href="{{ path('app_profile_gamemaster') }}">Meneur(euse) de jeu</a></li>
+            <li class="is-active"><a>Ludothèque</a></li>
+            <li><a href="{{ path('app_profile_gameadd') }}">Proposer un jeu</a></li>
+            {% endif %}
+        </ul>
+    </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>Description</th>
+                <th>Genres</th>
+                <th>Dans la ludothèque?</th>
+            </tr>
+            </thead>
+            <tbody>
+            {% for game in games %}
+                <tr>
+                    <th>{{ game.name }}</th>
+                    <td>{{ game.description }}</td>
+                    <td>{% for genre in game.genre %}<span class="tag is-info is-light">{{ genre.genre }}</span> {% endfor %}</td>
+                    <td>
+                        {% if game.isInAssoLibrary %}
+                            {% if game.isPhysical %}
+                                Version&nbsp;physique<br/>
+                            {% endif %}
+                            {% if game.isNumerical %}
+                                {% if game.getUrlNumericalVersion %}<a href="{{game.getUrlNumericalVersion}}" target="_blank">Version&nbsp;numérique</a>
+                                {% else %}Version&nbsp;numérique{% endif %}
+                            {% endif %}
+                        {% else %}
+                        Non
+                        {% endif %}
+
+                    </td>
+                </tr>
+            {% endfor %}
+            </tbody>
+
+        </table>
+    </div>
+
+
+{% endblock %}

+ 97 - 0
templates/profile/gamemaster.html.twig

@@ -0,0 +1,97 @@
+{% extends 'bulma.html.twig' %}
+
+{% block title %}Mon compte{% endblock %}
+
+{% block content %}
+
+    <nav class="breadcrumb has-arrow-separator" aria-label="breadcrumbs">
+      <ul>
+        <li><a href="{{ path('app_main') }}">Accueil</a></li>
+        <li class="is-active"><a href="{{ path('app_profile') }}">Mon compte</a></li>
+      </ul>
+    </nav>
+
+    <div class="tabs is-boxed">
+        <ul>
+            <li><a href="{{ path('app_profile') }}">Compte utilisateur(rice)</a></li>
+            {% if app.user.linkToGamemaster %}
+            <li class="is-active"><a>Meneur(euse) de jeu</a></li>
+            <li><a href="{{ path('app_profile_gamelist')}}">Ludothèque</a></li>
+            <li><a href="{{ path('app_profile_gameadd') }}">Proposer un jeu</a></li>
+            {% endif %}
+        </ul>
+    </div>
+
+
+    {{ form_errors(form) }}
+    {{ form_start(form) }}
+
+    <div class="columns">
+        <div class="column">
+            {{ form_row(form.preferedName) }}
+        </div>
+        <div class="column">
+            {{ form_row(form.slug) }}
+        </div>
+    </div>
+
+    {{ 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 gamemaster.picture %}
+                <div class="field mt-2">
+                <p><a href="{{ path('app_profile_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-1by1"/>
+                {% 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>
+
+
+    {{ form_widget(form) }}
+    
+
+
+
+
+    <div class="control">
+        <button class="button is-primary" type="submit">Envoyer</button>
+    </div>
+
+    {{ form_end(form) }}
+
+{% endblock %}

+ 30 - 0
templates/profile/games.html.twig

@@ -0,0 +1,30 @@
+{% extends 'bulma.html.twig' %}
+
+{% block title %}Mes parties{% endblock %}
+
+{% block content %}
+
+    <nav class="breadcrumb has-arrow-separator" aria-label="breadcrumbs">
+      <ul>
+        <li><a href="{{ path('app_main') }}">Accueil</a></li>
+        <li class="is-active"><a href="{{ path('app_profile_games') }}">Mes parties</a></li>
+      </ul>
+    </nav>
+
+    <div class="tabs is-boxed">
+        <ul>
+            <li class="is-active"><a>En tant que joueur(euse)</a></li>
+            {% if app.user.linkToGamemaster %}
+            <li><a>En tant que meneur(euse) de jeu</a></li>
+            {% endif %}
+            <li><a>Mes demandes</a></li>
+        </ul>
+
+
+    </div>
+
+
+
+
+
+{% endblock %}

+ 47 - 5
templates/profile/index.html.twig

@@ -1,13 +1,13 @@
 {% extends 'bulma.html.twig' %}
 
-{% block title %}Compte{% endblock %}
+{% block title %}Mon compte{% endblock %}
 
 {% block content %}
 
     <nav class="breadcrumb has-arrow-separator" aria-label="breadcrumbs">
       <ul>
         <li><a href="{{ path('app_main') }}">Accueil</a></li>
-        <li class="is-active"><a href="{{ path('app_profile') }}">Compte</a></li>
+        <li class="is-active"><a href="{{ path('app_profile') }}">Mon compte</a></li>
       </ul>
     </nav>
 
@@ -15,13 +15,55 @@
         <ul>
             <li class="is-active"><a>Compte utilisateur(rice)</a></li>
             {% if app.user.linkToGamemaster %}
-            <li><a>Meneur(euse) de jeu</a></li>
-            <li><a>Liste des jeux</a></lI>
-            <li><a>Proposer un jeu</a></li>
+            <li><a href="{{ path('app_profile_gamemaster') }}">Meneur(euse) de jeu</a></li>
+            <li><a href="{{ path('app_profile_gamelist')}}">Ludothèque</a></li>
+            <li><a href="{{ path('app_profile_gameadd') }}">Proposer un jeu</a></li>
             {% endif %}
         </ul>
     </div>
 
+    {{ form_start(form) }}
+
+
+      <div class="columns">
+        <div class="column">
+          <label class="label">Adresse email</label>
+          <span class="icon-text">{{ user.email }}{% if user.isVerified %}<span class="icon"><twig:ux:icon name="bi:check2-circle" /></span>{% endif %}</span>
+        </div>
+        <div class="column">
+          <label class="label">Rôle</label>
+          {{ component('Role', {roles: user.roles}) }}
+        </div>
+        <div class="column">
+          <label class="label">Dernière connexion</label>
+          {{ user.lastLogin ? user.lastLogin|date('d/m/Y H:i:s', app_timezone) : 'Jamais' }}
+        </div>
+      </div>
+
+    <div class="columns">
+      <div class="column">
+        {{ form_row(form.firstName) }}
+      </div>
+      <div class="column">
+              {{ form_row(form.lastName) }}  
+      </div>
+    </div>
+
+        {{ form_row(form.phone) }}
+        <div class="box">
+          <h3 class="title is-5">Changer votre mot de passe</h3>
+          <p >Laissez vide pour ne pas changer votre mot de passe.</p>
+          {{ form_row(form.newPassword) }}
+        </div>
+        <div class="control">
+        <button class="button is-primary" type="submit">Envoyer</button>
+        </div>
+
+    {{ form_end(form) }}
+
+
+
+
 
 
 {% endblock %}