diff --git a/config/packages/tailwind.yaml b/config/packages/tailwind.yaml new file mode 100644 index 0000000..58c90b0 --- /dev/null +++ b/config/packages/tailwind.yaml @@ -0,0 +1,2 @@ +symfonycasts_tailwind: + input_css: '%kernel.project_dir%/assets/styles/app.css' diff --git a/config/services.yaml b/config/services.yaml index b6fc8bf..b37d953 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -1,5 +1,8 @@ +parameters: + images_directory: '%kernel.project_dir%/public/uploads/images' + services: - # configuration par défaut pour les services +# configuration par défaut pour les services _defaults: autowire: true # Injecte automatiquement les dépendances dans vos services. autoconfigure: true # Enregistre automatiquement vos services en tant que commandes, abonnés d'événements, etc. @@ -11,4 +14,9 @@ services: arguments: $kazApiClient: '@kaz_api.client' $apiUser: '%env(KAZ_API_USER)%' - $apiPassword: '%env(KAZ_API_PASSWORD)%' \ No newline at end of file + $apiPassword: '%env(KAZ_API_PASSWORD)%' + + # Gestion de l'enregistrement de la photo de profil + App\Service\FileUploader: + arguments: + $targetDirectory: '%images_directory%' diff --git a/migrations/Version20260326214353.php b/migrations/Version20260326214353.php new file mode 100644 index 0000000..a2eddd8 --- /dev/null +++ b/migrations/Version20260326214353.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE "user" ADD photo 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 "user" DROP photo'); + } +} diff --git a/migrations/Version20260326231417.php b/migrations/Version20260326231417.php new file mode 100644 index 0000000..d7e8987 --- /dev/null +++ b/migrations/Version20260326231417.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE "user" ADD telephone VARCHAR(20) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE "user" DROP telephone'); + } +} diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index a8099a5..d3e61ca 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -3,14 +3,18 @@ namespace App\Controller; use App\Form\ChangePasswordType; +use App\Form\UserProfileType; +use App\Service\FileUploader; use App\Service\KazApiService; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\FormError; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; @@ -33,32 +37,73 @@ class UserController extends AbstractController * @throws TransportExceptionInterface */ -// #[Route('/user/{email}', name: 'app_user', methods: ['GET'])] -// public function index(string $email, KazApiService $apiClient): Response -// { -// $exist = $apiClient->getUserData($email); -// -// return $this->render('user/index.html.twig', [ -// 'exist' => $exist, -// ]); -// } +# #[Route('/user/{email}', name: 'app_user', methods: ['GET'])] +# public function index(string $email, KazApiService $apiClient): Response +# { +# $exist = $apiClient->getUserData($email); +# +# return $this->render('user/index.html.twig', [ +# 'exist' => $exist, +# ]); +# } - #[Route('/mon-profil', name: 'app_user', methods: ['GET'])] - public function index(KazApiService $apiClient): Response - { - // Récupération de l'utilisateur actuellement connecté +/* TODO : Param l'API avec un Serializer pour la lecture du fichier JSON ? */ + #[Route('/mon-profil', name: 'app_user', methods: ['GET', 'POST'])] + #[IsGranted('ROLE_USER')] + public function showProfile( + Request $request, + EntityManagerInterface $entityManager, + FileUploader $fileUploader # <-- On injecte ton super service ici + ): Response { + # Récupération de l'utilisateur actuellement connecté $user = $this->getUser(); - // Utilisation de son email pour interroger l'API - $userData = $apiClient->getUserData($user->getUserIdentifier()); + /* Utilisation des fixtures pour vérifier la mise en page. + TODO: modifier pour que ça communique avec l'API */ + # Création du formulaire lié à l'utilisateur connecté + $form = $this->createForm(UserProfileType::class, $user); + $form->handleRequest($request); + + # Traitement si l'utilisateur clique sur "Valider" + if ($form->isSubmitted() && $form->isValid()) { + + /** @var UploadedFile $imageFile */ + $imageFile = $form->get('image')->getData(); + + if ($imageFile) { + # Suppression de l'ancienne photo du serveur + $fileUploader->delete($user->getPhoto()); + + # Dépot de la nouvelle photo + $newFilename = $fileUploader->upload($imageFile); + + # Mise à jour de l'utilisateur avec le nouveau nom + $user->setPhoto($newFilename); + } + + # Sauvegarde en base de données + $entityManager->flush(); + + # Message de confirmation et rechargement de la page + $this->addFlash('success', 'Votre profil a été mis à jour avec succès !'); + + return $this->redirectToRoute('app_user'); + } + + # Affichage de la page return $this->render('user/index.html.twig', [ - 'userData' => $userData, + 'form' => $form->createView(), + 'userData' => $user, # TODO : Mettre $userData quand connexion avec API OK ]); } #[Route('/mot-de-passe', name: 'app_user_edit_password', methods: ['GET', 'POST'])] - public function editPassword(Request $request, UserPasswordHasherInterface $hasher, EntityManagerInterface $entityManager): Response + public function editPassword( + Request $request, + UserPasswordHasherInterface $hasher, + EntityManagerInterface $entityManager + ): Response { # Récupération de l'utilisateur actuellement connecté $user = $this->getUser(); diff --git a/src/Entity/User.php b/src/Entity/User.php index 8e8f827..8bb39ff 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -3,6 +3,7 @@ namespace App\Entity; use App\Repository\UserRepository; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; @@ -66,6 +67,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\Column(length: 255)] private ?string $firstName = null; + #[ORM\Column(length: 255, nullable: true)] + private ?string $photo = null; + + #[ORM\Column(length: 20, nullable: true)] + private ?string $telephone = null; + public function getId(): ?Uuid { return $this->id; @@ -275,4 +282,28 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + + public function getPhoto(): ?string + { + return $this->photo; + } + + public function setPhoto(?string $photo): static + { + $this->photo = $photo; + + return $this; + } + + public function getTelephone(): ?string + { + return $this->telephone; + } + + public function setTelephone(?string $telephone): static + { + $this->telephone = $telephone; + + return $this; + } } diff --git a/src/Form/ChangePasswordType.php b/src/Form/ChangePasswordType.php index bc67af5..a24c43d 100644 --- a/src/Form/ChangePasswordType.php +++ b/src/Form/ChangePasswordType.php @@ -17,26 +17,30 @@ class ChangePasswordType extends AbstractType { $builder ->add('oldPassword', PasswordType::class, [ - 'label' => 'Ancien mot de passe', + 'label' => 'Mot de passe actuel', 'mapped' => false, + # Mise en place de contraintes dans la saisie du mot de passe + 'constraints' => [ + new NotBlank(message: 'Veuillez saisir votre mot de passe actuel'), + ], ]) ->add('newPassword', RepeatedType::class, [ 'type' => PasswordType::class, + 'invalid_message' => 'Les deux mots de passe doivent être identiques.', 'mapped' => false, - 'first_options' => ['label' => 'Nouveau mot de passe'], - 'second_options' => ['label' => 'Confirmer mot de passe'], - # Mise en place de contraintes dans la saisie du mot de passe 'constraints' => [ - new NotBlank([ - 'message' => 'Veuillez saisir un mot de passe', - ]), - new Length([ - 'min' => 8, - 'minMessage' => 'Votre mot de passe doit faire au moins {{ limit }} caractères', - 'max' => 4096, - ]), + new NotBlank( + message: 'Veuillez saisir un mot de passe' + ), + new Length( + min: 8, + minMessage: 'Votre mot de passe doit faire au moins {{ limit }} caractères', + max: 4096, + ), ], + 'first_options' => ['label' => 'Nouveau mot de passe'], + 'second_options' => ['label' => 'Confirmer le nouveau mot de passe'], ]) ; } diff --git a/src/Form/UserProfileType.php b/src/Form/UserProfileType.php new file mode 100644 index 0000000..8307cae --- /dev/null +++ b/src/Form/UserProfileType.php @@ -0,0 +1,73 @@ +add('firstName', TextType::class, [ + 'label' => 'Prénom', + 'disabled' => true, + ]) + ->add('lastName', TextType::class, [ + 'label' => 'Nom', + 'disabled' => true, + ]) + ->add('email', EmailType::class, [ + 'label' => 'E-mail', + 'disabled' => true, + ]) + ->add('emailDeSecours', EmailType::class, ['label' => 'E-mail de secours']) + ->add('telephone', TelType::class, [ + 'label'=>'Téléphone', + 'required' => false, + 'attr' => [ + 'placeholder'=>'06 00 00 00 00', + 'class'=> 'w-full px-4 py-3 border border-gris-clair rounded-lg focus:outline-none focus:border-bouton focus:ring-1 focus:ring-bouton transition-shadow' + ], + 'constraints' => [ + new Regex( + pattern: '/^[0-9\+\s\.\-\(\)]+$/', + message: 'Le numéro de téléphone contient des caractères non valides' + ), + new Length( + max: 20, + maxMessage: 'Le numéro est trop long (maximum {{ limit }} caractères)' + ), + ], + ]) + ->add('image', FileType::class, [ + 'label' => 'Ma photo de profil', + 'mapped' => false, + 'required' => false, + 'constraints' => [ + new Image( + maxSize: '2M', + extensions: ['jpg', 'jpeg', 'png'], + extensionsMessage: 'Veuillez déposer une image JPG, JPEG ou PNG valide',) + ], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} diff --git a/src/Services/FileUploader.php b/src/Services/FileUploader.php new file mode 100644 index 0000000..d5675ca --- /dev/null +++ b/src/Services/FileUploader.php @@ -0,0 +1,48 @@ +getClientOriginalName(), PATHINFO_FILENAME); + $safeFilename = $this->slugger->slug($originalFilename); + $fileName = $safeFilename . '-' . uniqid() . '.' . $file->guessExtension(); + + try { + $file->move($this->getTargetDirectory(), $fileName); + } catch (FileException $e) { + // Ici tu peux logguer l'erreur si besoin + throw new \Exception('Erreur lors du transfert de l\'image : ' . $e->getMessage()); + } + + return $fileName; + } + + public function delete(?string $fileName): void + { + if ($fileName) { + $filePath = $this->getTargetDirectory() . '/' . $fileName; + if (file_exists($filePath)) { + unlink($filePath); + } + } + } + + public function getTargetDirectory(): string + { + return $this->targetDirectory; + } +} diff --git a/templates/base.html.twig b/templates/base.html.twig index 2090cb4..50788c4 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -4,7 +4,7 @@ - {% block title %}Association KAZ{% endblock %} + {% block title %}Association Kaz{% endblock %} {% block stylesheets %} @@ -17,20 +17,53 @@ - {# entête du site #} -
-
- {{ include('_navbar.html.twig') }} -
-
- {# contenu principal de chaque page #} -
-
- {% block body %} - {% endblock body %} -
+ + {% block navbar %} + {{ include('_navbar.html.twig') }} + {% endblock %} + + {# Contenu principal #} +
+ {# Affichage des messages flash (Succès ou Erreur) #} +
+ {% for label, messages in app.flashes %} + {% for message in messages %} + + {% endfor %} + {% endfor %} +
+ + {# Contenu principal de chaque page #} + {% block body %}{% endblock %} +
- {# pied-de-page du site #} + {# Gestion du pied-de-page du site #}