feat/login #10

Open
maurine wants to merge 12 commits from feat/login into main
23 changed files with 307 additions and 592 deletions
Showing only changes of commit b57236c4e7 - Show all commits

17
.env
View File

@@ -1,10 +1,11 @@
APP_ENV=dev APP_ENV=
APP_SECRET=je_te_remplis_parce_que_tu_me_mets_des_messages_d_erreur APP_SECRET=
APP_SHARE_DIR=var/share APP_SHARE_DIR=
APP_VERSION=0.0.1 APP_VERSION=
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8" DATABASE_URL=
MESSENGER_TRANSPORT_DSN="doctrine://default" MESSENGER_TRANSPORT_DSN=
MAILER_DSN="smtp://localhost:1025" MAILER_DSN=
DEFAULT_URI="http://localhost:8000" DEFAULT_URI=
KAZ_API_BASE_URL=
KAZ_API_USER= KAZ_API_USER=
KAZ_API_PASSWORD= KAZ_API_PASSWORD=

View File

@@ -30,6 +30,6 @@
--color-gris-fonce: #4B5563; --color-gris-fonce: #4B5563;
/* Polices */ /* Polices */
--font-sora: "Sora", system-ui, sans-serif; --font-sora: "Sora", sans-serif;
--font-caveat: "Caveat", cursive; --font-caveat: "Caveat", cursive;
} }

View File

@@ -16,7 +16,7 @@ services:
$apiUser: '%env(KAZ_API_USER)%' $apiUser: '%env(KAZ_API_USER)%'
$apiPassword: '%env(KAZ_API_PASSWORD)%' $apiPassword: '%env(KAZ_API_PASSWORD)%'
# Gestion de l'enregistrement de la photo de profil # Gestion de l'enregistrement de l'image de profil
App\Service\FileUploader: App\Service\FileUploader:
arguments: arguments:
$targetDirectory: '%images_directory%' $targetDirectory: '%images_directory%'

View File

@@ -1,35 +0,0 @@
<?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 Version20260313151403 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 "user" (id UUID NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, email_quota VARCHAR(255) NOT NULL, alternate_email VARCHAR(255) NOT NULL, identifiant_kaz VARCHAR(255) NOT NULL, quota VARCHAR(255) NOT NULL, has_nextcloud_access BOOLEAN NOT NULL, nextcloud_quota VARCHAR(255) NOT NULL, has_mobilizon BOOLEAN NOT NULL, has_agora_access BOOLEAN NOT NULL, lastname VARCHAR(255) NOT NULL, firstname VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)');
$this->addSql('CREATE TABLE messenger_messages (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, body TEXT NOT NULL, headers TEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, available_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, delivered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0E3BD61CE16BA31DBBF396750 ON messenger_messages (queue_name, available_at, delivered_at, id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE "user"');
$this->addSql('DROP TABLE messenger_messages');
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,33 +0,0 @@
<?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 Version20260316104557 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 "user" ALTER has_nextcloud_access DROP NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER has_mobilizon DROP NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ALTER has_nextcloud_access SET NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER has_mobilizon SET NOT NULL');
}
}

View File

@@ -1,49 +0,0 @@
<?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 Version20260316114715 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 "user" ADD last_name VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE "user" ADD first_name VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE "user" DROP lastname');
$this->addSql('ALTER TABLE "user" DROP firstname');
$this->addSql('ALTER TABLE "user" ALTER alternate_email SET NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER identifiant_kaz SET NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER quota SET NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER has_nextcloud_access SET NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER has_mobilizon SET NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER has_agora_access SET NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ADD lastname VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE "user" ADD firstname VARCHAR(255) NOT NULL');
$this->addSql('ALTER TABLE "user" DROP last_name');
$this->addSql('ALTER TABLE "user" DROP first_name');
$this->addSql('ALTER TABLE "user" ALTER alternate_email DROP NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER identifiant_kaz DROP NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER quota DROP NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER has_nextcloud_access DROP NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER has_mobilizon DROP NOT NULL');
$this->addSql('ALTER TABLE "user" ALTER has_agora_access DROP NOT NULL');
}
}

View File

@@ -1,31 +0,0 @@
<?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 Version20260326214353 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 "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');
}
}

View File

@@ -1,31 +0,0 @@
<?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 Version20260326231417 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 "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');
}
}

View File

@@ -1,35 +0,0 @@
<?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 Version20260328101039 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 "user" (id UUID NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, email_quota VARCHAR(255) NOT NULL, alternate_email VARCHAR(255) NOT NULL, identifiant_kaz VARCHAR(255) NOT NULL, quota VARCHAR(255) NOT NULL, has_nextcloud_access BOOLEAN NOT NULL, nextcloud_quota VARCHAR(255) NOT NULL, has_mobilizon BOOLEAN NOT NULL, has_agora_access BOOLEAN NOT NULL, last_name VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL, photo VARCHAR(255) DEFAULT NULL, telephone VARCHAR(20) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)');
$this->addSql('CREATE TABLE messenger_messages (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, body TEXT NOT NULL, headers TEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, available_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, delivered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0E3BD61CE16BA31DBBF396750 ON messenger_messages (queue_name, available_at, delivered_at, id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE "user"');
$this->addSql('DROP TABLE messenger_messages');
}
}

View File

@@ -1,35 +0,0 @@
<?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 Version20260328101220 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 "user" (id UUID NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, email_quota VARCHAR(255) NOT NULL, alternate_email VARCHAR(255) NOT NULL, identifiant_kaz VARCHAR(255) NOT NULL, quota VARCHAR(255) NOT NULL, has_nextcloud_access BOOLEAN NOT NULL, nextcloud_quota VARCHAR(255) NOT NULL, has_mobilizon BOOLEAN NOT NULL, has_agora_access BOOLEAN NOT NULL, last_name VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL, photo VARCHAR(255) DEFAULT NULL, telephone VARCHAR(20) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)');
$this->addSql('CREATE TABLE messenger_messages (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, body TEXT NOT NULL, headers TEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, available_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, delivered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0E3BD61CE16BA31DBBF396750 ON messenger_messages (queue_name, available_at, delivered_at, id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE "user"');
$this->addSql('DROP TABLE messenger_messages');
}
}

View File

@@ -10,7 +10,7 @@ use Doctrine\Migrations\AbstractMigration;
/** /**
* Auto-generated Migration: Please modify to your needs! * Auto-generated Migration: Please modify to your needs!
*/ */
final class Version20260329084928 extends AbstractMigration final class Version20260331084216 extends AbstractMigration
{ {
public function getDescription(): string public function getDescription(): string
{ {
@@ -20,8 +20,8 @@ final class Version20260329084928 extends AbstractMigration
public function up(Schema $schema): void public function up(Schema $schema): void
{ {
// this up() migration is auto-generated, please modify it to your needs // this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE "user" (id UUID NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, email_quota VARCHAR(255) NOT NULL, alternate_email VARCHAR(255) NOT NULL, identifiant_kaz VARCHAR(255) NOT NULL, quota VARCHAR(255) NOT NULL, has_nextcloud_access BOOLEAN NOT NULL, nextcloud_quota VARCHAR(255) NOT NULL, has_mobilizon BOOLEAN NOT NULL, has_agora_access BOOLEAN NOT NULL, last_name VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL, photo VARCHAR(255) DEFAULT NULL, telephone VARCHAR(20) DEFAULT NULL, PRIMARY KEY (id))'); $this->addSql('CREATE TABLE "user" (id UUID NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, email_quota VARCHAR(255) NOT NULL, alternate_email VARCHAR(255) NOT NULL, identifiant_kaz VARCHAR(255) NOT NULL, quota VARCHAR(255) NOT NULL, has_nextcloud_access BOOLEAN NOT NULL, nextcloud_quota VARCHAR(255) NOT NULL, has_mobilizon BOOLEAN NOT NULL, has_agora_access BOOLEAN NOT NULL, last_name VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL, image VARCHAR(255) DEFAULT NULL, telephone VARCHAR(20) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)'); $this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D649E7927C74 ON "user" (email)');
$this->addSql('CREATE TABLE messenger_messages (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, body TEXT NOT NULL, headers TEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, available_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, delivered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY (id))'); $this->addSql('CREATE TABLE messenger_messages (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, body TEXT NOT NULL, headers TEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, available_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, delivered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0E3BD61CE16BA31DBBF396750 ON messenger_messages (queue_name, available_at, delivered_at, id)'); $this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0E3BD61CE16BA31DBBF396750 ON messenger_messages (queue_name, available_at, delivered_at, id)');
} }

View File

@@ -7,10 +7,11 @@ use App\Form\UserProfileType;
use App\Service\FileUploader; use App\Service\FileUploader;
use App\Service\KazApiService; use App\Service\KazApiService;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Exception;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
@@ -45,8 +46,6 @@ class UserController extends AbstractController
'user' => $user, 'user' => $user,
]); ]);
} }
/* TODO : Param l'API avec un Serializer pour la lecture du fichier JSON ? */
#[Route('/mon-profil', name: 'app_user', methods: ['GET', 'POST'])] #[Route('/mon-profil', name: 'app_user', methods: ['GET', 'POST'])]
#[IsGranted('ROLE_USER')] #[IsGranted('ROLE_USER')]
public function showProfile( public function showProfile(
@@ -54,66 +53,54 @@ class UserController extends AbstractController
EntityManagerInterface $entityManager, EntityManagerInterface $entityManager,
FileUploader $fileUploader, FileUploader $fileUploader,
KazApiService $apiKazService KazApiService $apiKazService
): Response { ): Response
# Récupération de l'utilisateur actuellement connecté {
// Récupération de l'utilisateur actuellement connecté
$user = $this->getUser(); $user = $this->getUser();
// Vérification si l'URL est en mode édition
$isEditMode = $request->query->getBoolean('edit', false);
$kazUser = $apiKazService->getUserData($user->getEmail()); try {
// Récupération des données de l'utilisateur sur l'API grâce à son email
$kazUser = $apiKazService->getUserData($user->getEmail());
// Initialisation de la variable $userData
$user = $user->updateFromKazUser($kazUser);
} catch (Exception $e) {
$this->addFlash('error', 'Impossible de charger vos données.');
}
$user = $user->updateFromKazUser($kazUser); // Création du formulaire lié à l'utilisateur connecté
//TODO: modifier pour que ça communique avec l'API */
# Création du formulaire lié à l'utilisateur connecté
$form = $this->createForm(UserProfileType::class, $user); $form = $this->createForm(UserProfileType::class, $user);
$form->handleRequest($request); $form->handleRequest($request);
# Traitement si l'utilisateur clique sur "Valider" // Affichage du formulaire si les données sont valides
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
/** @var UploadedFile|null $imageFile */
/** @var UploadedFile $imageFile */
$imageFile = $form->get('image')->getData(); $imageFile = $form->get('image')->getData();
// --- Gestion de l'image de profil ---
if ($imageFile) { if ($imageFile) {
# Suppression de l'ancienne photo du serveur // Suppression de l'ancienne image via le service
$fileUploader->delete($user->getPhoto()); if ($user->getImage()) {
$fileUploader->delete($user->getImage());
# Dépot de la nouvelle photo }
// Dépôt de la nouvelle image et mise à jour de son nom dans l'entité
$newFilename = $fileUploader->upload($imageFile); $newFilename = $fileUploader->upload($imageFile);
$user->setImage($newFilename);
# Mise à jour de l'utilisateur avec le nouveau nom
$user->setPhoto($newFilename);
} }
$alternateEmail = $form->get('alternateEmail')->getData(); // Sauvegarde en base de données
$regexEmail = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';
if(isset($alternateEmail) && preg_match($regexEmail, $alternateEmail)) {
$user->setAlternateEmail($form->get('alternateEmail')->getData());
} else {
$alternateEmail->addError(new FormError('L\'adresse e-mail n\'est pas valide.'));
}
$telephone = $form->get('telephone')->getData();
$regexTelephone = '/^[0-9\+\s\.\-\(\)]+$/';
if(isset($telephone) && preg_match($regexTelephone, $telephone)) {
$user->setTelephone($telephone);
} else {
$telephone->addError(new FormError('Le numéro de téléphone n\'est pas valide.'));
}
# Sauvegarde en base de données
$entityManager->flush(); $entityManager->flush();
// Message de confirmation et rechargement de la page
# Message de confirmation et rechargement de la page
$this->addFlash('success', 'Votre profil a été mis à jour avec succès !'); $this->addFlash('success', 'Votre profil a été mis à jour avec succès !');
// Redirection de l'utilisateur
return $this->redirectToRoute('app_user'); return $this->redirectToRoute('app_user');
} }
// Affichage de la page
# Affichage de la page
return $this->render('user/index.html.twig', [ return $this->render('user/index.html.twig', [
'form' => $form->createView(), 'form' => $form->createView(),
'userData' => $user, # TODO : Mettre $userData quand connexion avec API OK 'userData' => $user,
'isEditMode' => $isEditMode,
]); ]);
} }
@@ -124,31 +111,30 @@ class UserController extends AbstractController
EntityManagerInterface $entityManager EntityManagerInterface $entityManager
): Response ): Response
{ {
# Création du formulaire // Création du formulaire
$form = $this->createForm(ChangePasswordType::class); $form = $this->createForm(ChangePasswordType::class);
# Liaison du formulaire à la requête HTTP // Liaison du formulaire à la requête HTTP
$form->handleRequest($request); $form->handleRequest($request);
# Vérification du formulaire, s'il est bien soumis et valide // Vérification du formulaire, s'il est bien soumis et valide
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
# Récupération des données du formulaire // Récupération des données du formulaire
$user = $this->getUser(); $user = $this->getUser();
$plainOldPassword = $form->get('oldPassword')->getData(); $plainOldPassword = $form->get('oldPassword')->getData();
$newPassword = $form->get('newPassword')->getData(); $newPassword = $form->get('newPassword')->getData();
# Vérification de l'ancien mot de passe // Vérification de l'ancien mot de passe
if (!$hasher->isPasswordValid($user, $plainOldPassword)) { if (!$hasher->isPasswordValid($user, $plainOldPassword)) {
$form->get('oldPassword')->addError(new FormError('L\'ancien mot de passe est incorrect.')); $form->get('oldPassword')->addError(new FormError('L\'ancien mot de passe est incorrect.'));
} else { } else {
# Si tout est OK : Hachage du mot de passe // Si tout est OK : Hachage du mot de passe
$hashedPassword = $hasher->hashPassword($user, $newPassword); $hashedPassword = $hasher->hashPassword($user, $newPassword);
$user->setPassword($hashedPassword); $user->setPassword($hashedPassword);
# Sauvegarde en BDD // Sauvegarde en BDD
$entityManager->flush(); $entityManager->flush();
// Message de succès pour l'utilisateur
# Message de succès pour l'utilisateur
$this->addFlash('success', 'Votre mot de passe a bien été mis à jour !'); $this->addFlash('success', 'Votre mot de passe a bien été mis à jour !');
return $this->redirectToRoute('app_user_edit_password'); return $this->redirectToRoute('app_user_edit_password');

View File

@@ -3,7 +3,6 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
@@ -69,9 +68,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Column(length: 255, name: 'first_name')] #[ORM\Column(length: 255, name: 'first_name')]
private ?string $firstName = null; private ?string $firstName = null;
// TODO: Modifier "photo" par "image" #[ORM\Column(length: 255, nullable: true, name: 'image')]
#[ORM\Column(length: 255, nullable: true, name: 'photo')] private ?string $image = null;
private ?string $photo = null;
#[ORM\Column(length: 20, nullable: true, name: 'telephone')] #[ORM\Column(length: 20, nullable: true, name: 'telephone')]
private ?string $telephone = null; private ?string $telephone = null;
@@ -290,14 +288,14 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this; return $this;
} }
public function getPhoto(): ?string public function getImage(): ?string
{ {
return $this->photo; return $this->image;
} }
public function setPhoto(?string $photo): static public function setImage(?string $image): static
{ {
$this->photo = $photo; $this->image = $image;
return $this; return $this;
} }
@@ -314,17 +312,27 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this; return $this;
} }
// Fonction qui permet d'afficher les données de l'API sur la page de profil
public function updateFromKazUser($kazUser) : User public function updateFromKazUser($kazUser) : User
{ {
// Récupération et conversion des données de l'API pour les afficher
$this->setEmail($kazUser['mail']); $this->setEmail($kazUser['mail']);
// Création du firstname et lastname // Création du firstname et lastname (une seule donnée sur l'API)
$name = explode(' ', $kazUser['sn']); $name = explode(' ', $kazUser['sn']);
$this->setFirstName($name[0]); $this->setFirstName($name[0]);
// Récupération des valeurs du tableau moins la première // Récupération des valeurs du tableau moins la première
$aLastname = array_slice($name, 1); $aLastname = array_slice($name, 1);
$this->setLastName(implode(' ', $aLastname)); $this->setLastName(implode(' ', $aLastname));
// Récupération du mail de secours
//TODO: Ajouter les champs manquants de l'objet User dans l'api kaz. $this->setAlternateEmail($kazUser['mailDeSecours']);
$this->setEmailQuota($kazUser['mailQuota']);
$this->setHasAgoraAccess($kazUser['agoraEnabled']);
$this->setHasMobilizon($kazUser['mobilizonEnabled']);
$this->setHasNextcloudAccess($kazUser['nextcloudEnabled']);
$this->setNextcloudQuota($kazUser['nextcloudQuota']);
$this->setQuota($kazUser['quota']);
$this->setIdentifiantKaz($kazUser['identifiantKaz']);
$this->setTelephone($kazUser['telephone'] ?? null);
return $this; return $this;
} }

View File

@@ -3,11 +3,12 @@
namespace App\Form; namespace App\Form;
use App\Entity\User; use App\Entity\User;
use Symfony\Component\Form\Extension\Core\Type\TelType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TelType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Image; use Symfony\Component\Validator\Constraints\Image;
@@ -27,22 +28,53 @@ class UserProfileType extends AbstractType
'label' => 'Nom', 'label' => 'Nom',
'disabled' => true, 'disabled' => true,
]) ])
->add('identifiantKaz', TextType::class, [
'label' => 'Identifiant KAZ : ',
'disabled' => true,
])
->add('email', EmailType::class, [ ->add('email', EmailType::class, [
'label' => 'E-mail', 'label' => 'E-mail',
'disabled' => true, 'disabled' => true,
]) ])
->add('alternateEmail', EmailType::class, ['label' => 'E-mail de secours']) ->add('alternateEmail', EmailType::class, [
'label' => 'E-mail de secours',
'constraints' => [
new Regex(
pattern: '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/',
message: 'L\'adresse e-mail n\'est pas valide.'
)
]
])
->add('emailQuota', TextType::class, [
'label' => 'Espace de stockage de votre boîte mail : ',
'disabled' => true,
])
->add('hasNextcloudAccess', CheckboxType::class, [
'label' => 'Accès au Nextcloud : ',
'disabled' => true,
])
->add('nextcloudQuota', TextType::class, [
'label' => 'Espace de stockage de votre Nextcloud : ',
'disabled' => true,
])
->add('hasMobilizon', CheckboxType::class, [
'label' => 'Accès à Mobilizon : ',
'disabled' => true,
])
->add('hasAgoraAccess', CheckboxType::class, [
'label' => 'Accès à l\'Agora : ',
'disabled' => true,
])
->add('telephone', TelType::class, [ ->add('telephone', TelType::class, [
'label'=>'Téléphone', 'label'=>'Téléphone',
'required' => false, 'required' => false,
'attr' => [ 'attr' => [
'placeholder'=>'06 00 00 00 00', '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' => [ 'constraints' => [
new Regex( new Regex(
pattern: '/^[0-9\+\s\.\-\(\)]+$/', pattern: '/^[0-9\+\s\.\-\(\)]+$/',
message: 'Le numéro de téléphone contient des caractères non valides' message: 'Le numéro de téléphone n\'est pas valide.'
), ),
new Length( new Length(
max: 20, max: 20,
@@ -51,7 +83,7 @@ class UserProfileType extends AbstractType
], ],
]) ])
->add('image', FileType::class, [ ->add('image', FileType::class, [
'label' => 'Ma photo de profil', 'label' => 'Mon image de profil',
'mapped' => false, 'mapped' => false,
'required' => false, 'required' => false,
'constraints' => [ 'constraints' => [

View File

@@ -2,45 +2,76 @@
namespace App\Service; namespace App\Service;
use RuntimeException;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\String\Slugger\SluggerInterface; use Symfony\Component\String\Slugger\SluggerInterface;
/**
* Service de gestion des envois et suppressions de fichiers.
*/
class FileUploader class FileUploader
{ {
// On utilise la promotion de constructeur (PHP 8) : ultra moderne et concis /**
* @param string $targetDirectory Le chemin absolu vers le dossier de dépôt.
* @param SluggerInterface $slugger Le service de nettoyage des chaînes de caractères.
*/
public function __construct( public function __construct(
private string $targetDirectory, #[Autowire('%kernel.project_dir%/public/uploads/img')]
private SluggerInterface $slugger, private readonly string $targetDirectory,
) { private readonly SluggerInterface $slugger,
)
{
} }
/**
* Traite, sécurise et déplace un fichier déposé.
*
* @param UploadedFile $file Le fichier physique à déposer.
* @return string Le nom final sécurisé et unique du fichier.
* @throws RuntimeException En cas d'échec de l'écriture sur le disque.
*/
public function upload(UploadedFile $file): string public function upload(UploadedFile $file): string
{ {
$originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME); $originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
$safeFilename = $this->slugger->slug($originalFilename); $safeFilename = $this->slugger->slug($originalFilename);
$fileName = $safeFilename . '-' . uniqid() . '.' . $file->guessExtension();
// Utilisation de uniqid('', true) pour garantir une unicité absolue en production
$fileName = sprintf('%s-%s.%s', $safeFilename, uniqid('', true), $file->guessExtension());
try { try {
$file->move($this->getTargetDirectory(), $fileName); $file->move($this->getTargetDirectory(), $fileName);
} catch (FileException $e) { } catch (FileException $e) {
// Ici tu peux logguer l'erreur si besoin throw new RuntimeException('Erreur lors du transfert de l\'image : ' . $e->getMessage(), 0, $e);
throw new \Exception('Erreur lors du transfert de l\'image : ' . $e->getMessage());
} }
return $fileName; return $fileName;
} }
/**
* Supprime physiquement un fichier du serveur.
*
* @param string|null $fileName Le nom du fichier à supprimer.
*/
public function delete(?string $fileName): void public function delete(?string $fileName): void
{ {
if ($fileName) { if (null === $fileName) {
$filePath = $this->getTargetDirectory() . 'FileUploader.php/' . $fileName; return;
if (file_exists($filePath)) { }
unlink($filePath);
} $filePath = rtrim($this->getTargetDirectory(), '/') . '/' . $fileName;
if (file_exists($filePath)) {
unlink($filePath);
} }
} }
/**
* Retourne le chemin du répertoire de dépôt.
*
* @return string
*/
public function getTargetDirectory(): string public function getTargetDirectory(): string
{ {
return $this->targetDirectory; return $this->targetDirectory;

View File

@@ -50,7 +50,7 @@ class KazApiService
} }
$data = $response->toArray(); $data = $response->toArray();
$this->token = $data['access_token']; // Ajustez la clé selon le format de votre API $this->token = $data['access_token'];
return $this->token; return $this->token;
} }

View File

@@ -16,8 +16,8 @@
Bienvenue sur ton espace kaznaute <span class="text-bouton">{{ app.user ? app.user.userIdentifier : 'visiteur' }}</span> ! Bienvenue sur ton espace kaznaute <span class="text-bouton">{{ app.user ? app.user.userIdentifier : 'visiteur' }}</span> !
</h2> </h2>
{# Zone réservée pour les futures données de l'API {# Zone réservée pour les futures données de l'API et Pahéko
TODO : Gérer les données de l'API #} TODO : Gérer les données avec Pahéko, mise en service en cours par un des développeurs de l'association. Cela sera vu à posteriori du stage #}
<div class="bg-bouton/10 border border-bouton/30 rounded-lg p-5"> <div class="bg-bouton/10 border border-bouton/30 rounded-lg p-5">
<h3 class="font-semibold text-title mb-3 flex items-center gap-2"> <h3 class="font-semibold text-title mb-3 flex items-center gap-2">
Votre abonnement actuellement : Votre abonnement actuellement :

View File

@@ -1,135 +1,165 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}Accueil | {{ parent() }}{% endblock %} {% block title %}Ma page de profil | {{ parent() }}{% endblock %}
{% block body %} {% block body %}
<div class="min-h-screen bg-bg-primaire py-8 w-full font-sora"> <div class="min-h-screen bg-bg-primaire py-8 w-full font-sora">
{# Affichage du formulaire (seulement en mode édition) #}
{% if isEditMode %}
{{ form_start(form, {'attr': {'class': 'max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 grid md:grid-cols-3 gap-8'}}) }} {{ form_start(form, {'attr': {'class': 'max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 grid md:grid-cols-3 gap-8'}}) }}
{% else %}
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 grid md:grid-cols-3 gap-8">
{% endif %}
{# TODO: voir si c'est pertinent avec l'API et s'il y a l'utilité d'une photo de profil #} {# Colonne Photo #}
{# Gestion de la colone avec le choix de la photo de profil #} <div class="flex flex-col text-text items-center">
<div class="flex flex-col text-text items-center"> <div class="w-full md:w-64 flex-shrink-0 mt-20">
<div class="border-2 border-black p-1 bg-white shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]">
{% if userData.image %}
<img src="{{ asset('uploads/images/' ~ userData.image) }}" alt="Photo de profil"
class="w-full aspect-[4/3] object-cover">
{% else %}
<div class="w-full aspect-[4/3] bg-gray-50 flex items-center justify-center text-6xl">👤
</div>
{% endif %}
</div>
</div>
{# Affichage de la photo de profil #} <p class="text-2xl text-title font-caveat mt-4"> Ma photo</p>
<div class="w-full md:w-64 flex-shrink-0 mt-20">
<div class="border-2 border-black p-1 bg-white shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]"> {# Affichage du champs "choisir en fichier" (seulement en mode édition) #}
{% if userData.photo %} {% if isEditMode %}
<img src="{{ asset('uploads/images/' ~ userData.photo) }}" <div class="w-full mt-2">
alt="Photo de profil" {{ form_label(form.image, 'Choisir un fichier', {'label_attr': {'class': 'block mb-2.5 text-sm font-medium text-gris-fonce'}}) }}
class="w-full aspect-[4/3] object-cover"> {{ form_widget(form.image, {
{% else %} 'attr': {'class': 'cursor-pointer bg-white border border-gris-clair text-text text-sm rounded-lg focus:outline-none focus:ring-1 focus:ring-bouton focus:border-bouton block w-full shadow-sm placeholder-gris-moyen file:mr-4 file:py-2.5 file:px-4 file:border-0 file:border-r file:border-gris-clair file:bg-gris-clair file:text-gris-fonce hover:file:bg-gris-moyen transition-colors'}
<div class="w-full aspect-[4/3] bg-gray-50 flex items-center justify-center text-6xl"> }) }}
👤 <p class="mt-1 text-sm text-gris-moyen">JPG, JPEG ou PNG (Taille max : 2Mo).</p>
<div class="text-red-500 text-xs mt-1 italic font-sora">{{ form_errors(form.image) }}</div>
</div>
{% endif %}
</div>
{# Colonne Infos Persos #}
<div class="md:col-span-2">
{# Gestion des boutons d'action (Modifier / Annuler) et des titres #}
<div class="relative flex items-center justify-center mb-6">
<h1 class="text-4xl font-caveat text-text m-0">Mon profil</h1>
<div class="absolute right-0">
{% if isEditMode %}
<a href="{{ path('app_user') }}"
class="flex items-center gap-1.5 px-3 py-1.5 bg-white border border-gris-clair hover:bg-gray-50 text-text text-sm font-bold rounded-md shadow-sm transition-colors">
❌ Annuler
</a>
{% else %}
<a href="{{ path('app_user', {'edit': 1}) }}"
class="flex items-center gap-1.5 px-3 py-1.5 bg-white border border-gris-clair hover:bg-gray-50 text-text text-sm font-bold rounded-md shadow-sm transition-colors">
✏️ Modifier
</a>
{% endif %}
</div>
</div>
<h2 class="text-2xl font-caveat text-text mb-6 text-center">Mes informations personnelles</h2>
<div class="flex flex-col gap-6">
{# Identifiant #}
<div class="space-y-1">
<label class="block text-sm font-semibold text-text">Identifiant KAZ:</label>
{% if isEditMode %}
{{ form_widget(form.identifiantKaz, {'attr': {'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 placeholder-gris-moyen transition-shadow'}}) }}
<div class="text-red-500 text-xs mt-1 italic">{{ form_errors(form.identifiantKaz) }}</div>
{% else %}
<div
class="w-full px-4 py-3 bg-gray-50 border border-gris-clair rounded-lg text-text">{{ userData.identifiantKaz ?? 'Non renseigné' }}</div>
{% endif %}
</div>
{# --- NOM et Prénom --- #}
<div class="grid grid-cols-2 gap-4">
{# NOM #}
<div class="space-y-1">
<label class="block text-sm font-semibold text-text">NOM :</label>
{% if isEditMode %}
{{ form_widget(form.lastName, {'attr': {'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 placeholder-gris-moyen transition-shadow'}}) }}
<div
class="text-red-500 text-xs mt-1 italic font-sora">{{ form_errors(form.lastName) }}</div>
{% else %}
<div
class="w-full px-4 py-3 bg-gray-50 border border-gris-clair rounded-lg text-text">{{ userData.lastName }}</div>
{% endif %}
</div>
{# Prénom #}
<div class="space-y-1">
<label class="block text-sm font-semibold text-text">Prénom :</label>
{% if isEditMode %}
{{ form_widget(form.firstName, {'attr': {'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 placeholder-gris-moyen transition-shadow'}}) }}
<div
class="text-red-500 text-xs mt-1 italic font-sora">{{ form_errors(form.firstName) }}</div>
{% else %}
<div
class="w-full px-4 py-3 bg-gray-50 border border-gris-clair rounded-lg text-text">{{ userData.firstName }}</div>
{% endif %}
</div>
</div>
{# Téléphone #}
<div class="space-y-1">
<label class="block text-sm font-semibold text-text">Numéro de téléphone :</label>
{% if isEditMode %}
{{ form_widget(form.telephone, {'attr': {'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 placeholder-gris-moyen transition-shadow'}}) }}
<div class="text-red-500 text-xs mt-1 italic">{{ form_errors(form.telephone) }}</div>
{% else %}
<div
class="w-full px-4 py-3 bg-gray-50 border border-gris-clair rounded-lg text-text">{{ userData.telephone ?? 'Non renseigné' }}</div>
{% endif %}
</div>
{# E-mail #}
<div class="space-y-1">
<label class="block text-sm font-semibold text-text">E-mail :</label>
{% if isEditMode %}
{{ form_widget(form.email, {'attr': {'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 placeholder-gris-moyen transition-shadow'}}) }}
<div class="text-red-500 text-xs mt-1 italic font-sora">{{ form_errors(form.email) }}</div>
{% else %}
<div
class="w-full px-4 py-3 bg-gray-50 border border-gris-clair rounded-lg text-text">{{ userData.email }}</div>
{% endif %}
</div>
{# E-mail de secours #}
<div class="space-y-1">
<label class="block text-sm font-semibold text-text">E-mail de secours :</label>
{% if isEditMode %}
{{ form_widget(form.alternateEmail, {'attr': {'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 placeholder-gris-moyen transition-shadow'}}) }}
<div
class="text-red-500 text-xs mt-1 italic font-sora">{{ form_errors(form.alternateEmail) }}</div>
{% else %}
<div
class="w-full px-4 py-3 bg-gray-50 border border-gris-clair rounded-lg text-text">{{ userData.alternateEmail ?? 'Non renseigné' }}</div>
{% endif %}
</div>
{# Affichage du bouton "Valider" (seulement en mode édition) #}
{% if isEditMode %}
<div class="flex flex-col sm:flex-row gap-4 pt-2">
<button type="submit"
class="flex-1 py-3 bg-bouton hover:bg-bouton-hover text-text font-bold rounded-lg shadow transition-colors">
Enregistrer les modifications
</button>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<p class="text-2xl text-title font-caveat mt-4"> Ma photo</p> {# Fermuture du formulaire (seulement en mode édition) #}
{% if isEditMode %}
{# Gestion du dépôt d'un fichier image #} {{ form_end(form) }}
<div class="w-full mt-2"> {% else %}
{{ form_label(form.image, 'Choisir un fichier', {
'label_attr': {'class': 'block mb-2.5 text-sm font-medium text-gris-fonce'}
}) }}
{{ form_widget(form.image, {
'attr': {
'class': 'cursor-pointer bg-white border border-gris-clair text-text text-sm rounded-lg focus:outline-none focus:ring-1 focus:ring-bouton focus:border-bouton block w-full shadow-sm placeholder-gris-moyen file:mr-4 file:py-2.5 file:px-4 file:border-0 file:border-r file:border-gris-clair file:bg-gris-clair file:text-gris-fonce hover:file:bg-gris-moyen file:cursor-pointer transition-colors',
'aria-describedby': 'file_input_help'
}
}) }}
<p class="mt-1 text-sm text-gris-moyen" id="file_input_help">
JPG, JPEG ou PNG (Taille max : 2Mo).
</p>
</div>
</div> </div>
{% endif %}
{# Gestion de la colonne avec les "infos persos" #}
<div class="md:col-span-2">
<h1 class="text-4xl font-caveat text-text mb-6 text-center sm:text-center">Mon profil</h1>
<h2 class="text-2xl font-caveat text-text mb-6 text-center sm:text-center">Mes informations personnelles</h2>
{# Gestion du formulaire qui regroupe toutes les infos perso #}
<div class="flex flex-col gap-6">
{# Champ NOM et Prénom #}
<div class="grid grid-cols-2 gap-4">
<div class="space-y-1">
{{ form_label(form.firstName, 'NOM :', {
'label_attr': {'class': 'block text-sm font-semibold text-text'}
}) }}
{{ form_widget(form.firstName, {
'attr': {'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 placeholder-gris-moyen transition-shadow'}
}) }}
{# Implémentation d'un message d'errer en cas de problème #}
<div class="text-red-500 text-xs mt-1 italic font-sora">
{{ form_errors(form.firstName) }}
</div>
</div>
<div class="space-y-1">
{{ form_label(form.lastName, 'Prénom :', {
'label_attr': {'class': 'block text-sm font-semibold text-text'}
}) }}
{{ form_widget(form.lastName, {
'attr': {'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 placeholder-gris-moyen transition-shadow'}
}) }}
{# Implémentation d'un message d'errer en cas de problème #}
<div class="text-red-500 text-xs mt-1 italic font-sora">
{{ form_errors(form.lastName) }}
</div>
</div>
</div>
{# Champ Téléphone #}
<div class="space-y-1">
{{ form_label(form.telephone, 'Numéro de téléphone', {
'label_attr': {'class': 'block text-sm font-semibold text-text'}
}) }}
{{ form_widget(form.telephone) }}
{# Implémentation d'un message d'errer en cas de problème #}
<div class="text-red-500 text-xs mt-1 italic">
{{ form_errors(form.telephone) }}
</div>
</div>
{# Champ E-mail #}
<div class="space-y-1">
{{ form_label(form.email, 'E-mail :', {
'label_attr': {'class': 'block text-sm font-semibold text-text'}
}) }}
{{ form_widget(form.email, {
'attr': {'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 placeholder-gris-moyen transition-shadow'}
}) }}
{# Implémentation d'un message d'errer en cas de problème #}
<div class="text-red-500 text-xs mt-1 italic font-sora">
{{ form_errors(form.email) }}
</div>
</div>
{# Champ E-mail de secours #}
<div class="space-y-1">
{{ form_label(form.alternateEmail, 'E-mail de secours :', {
'label_attr': {'class': 'block text-sm font-semibold text-text'}
}) }}
{{ form_widget(form.alternateEmail, {
'attr': {'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 placeholder-gris-moyen transition-shadow'}
}) }}
{# Implémentation d'un message d'errer en cas de problème #}
<div class="text-red-500 text-xs mt-1 italic font-sora">
{{ form_errors(form.alternateEmail) }}
</div>
</div>
<div class="flex flex-col sm:flex-row gap-4 pt-2">
<button type="submit"
class="flex-1 py-3 bg-bouton hover:bg-bouton-hover text-text font-bold rounded-lg shadow transition-colors">
Valider
</button>
</div>
</div>
</div>
{{ form_end(form) }}
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,6 +1,6 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}Accueil | {{ parent() }}{% endblock %} {% block title %}Ma page de profil | {{ parent() }}{% endblock %}
{% block body %} {% block body %}
<div class="min-h-screen bg-bg-primaire py-8 w-full font-sora"> <div class="min-h-screen bg-bg-primaire py-8 w-full font-sora">