feat/login #10

Open
maurine wants to merge 28 commits from feat/login into main
32 changed files with 788 additions and 835 deletions
+9 -8
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=
2
+36 -1
View File
@@ -5,6 +5,41 @@ import './stimulus_bootstrap.js';
* We recommend including the built version of this JavaScript file * We recommend including the built version of this JavaScript file
* (and its CSS file) in your base layout (base.html.twig). * (and its CSS file) in your base layout (base.html.twig).
*/ */
// any CSS you import will output into a single css file (app.css in this case) // any CSS you import will output into a single css file (app.css in this case)
import './styles/app.css'; import './styles/app.css';
// --- Gestion du responsive de la barre de navigation (menu hamburger) --- //
document.addEventListener('click', (event) => {
// Vérification : l'utilisateur a-t-il cliqué sur un élément interactif du menu ?
const isClickOnButton = event.target.closest('#mobile-menu-button');
const isClickOnLink = event.target.closest('#main-menu a');
// Si le clic a lieu ailleurs sur la page, on arrête tout
if (!isClickOnButton && !isClickOnLink) return;
// Si l'utilisateur a bien cliqué sur un élément interactif, on récupère les éléments du DOM
const menu = document.getElementById('main-menu');
const btn = document.getElementById('mobile-menu-button');
const iconClosed = document.getElementById('icon-menu-closed');
const iconOpen = document.getElementById('icon-menu-open');
// Option 1 : Clic sur le bouton hamburger
if (isClickOnButton && menu) {
menu.classList.toggle('hidden');
iconClosed.classList.toggle('hidden');
iconOpen.classList.toggle('hidden');
btn.setAttribute('aria-expanded', !menu.classList.contains('hidden'));
return;
}
// Option 2 : Clic sur un onglet du menu
if (isClickOnLink && menu && !menu.classList.contains('hidden')) {
menu.classList.add('hidden');
iconClosed.classList.remove('hidden');
iconOpen.classList.add('hidden');
btn.setAttribute('aria-expanded', 'false');
}
});
+1 -8
View File
@@ -1,13 +1,6 @@
import { startStimulusApp } from '@symfony/stimulus-bundle'; import {startStimulusApp} from '@symfony/stimulus-bundle';
const app = startStimulusApp(); const app = startStimulusApp();
import { startStimulusApp } from '@symfony/stimulus-bridge';
// Registers Stimulus controllers from controllers.json and in the controllers/ directory
export const app = startStimulusApp(require.context(
'@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
true,
/\.[jt]sx?$/
));
// register any custom, 3rd party controllers here // register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController); // app.register('some_controller_name', SomeImportedController);
+1 -1
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;
} }
Generated
+6 -6
View File
@@ -3686,16 +3686,16 @@
}, },
{ {
"name": "symfony/http-client", "name": "symfony/http-client",
"version": "v8.0.5", "version": "v8.0.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/http-client.git", "url": "https://github.com/symfony/http-client.git",
"reference": "f9fdd372473e66469c6d32a4ed12efcffdea38c4" "reference": "356e43d6994ae9d7761fd404d40f78691deabe0e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/f9fdd372473e66469c6d32a4ed12efcffdea38c4", "url": "https://api.github.com/repos/symfony/http-client/zipball/356e43d6994ae9d7761fd404d40f78691deabe0e",
"reference": "f9fdd372473e66469c6d32a4ed12efcffdea38c4", "reference": "356e43d6994ae9d7761fd404d40f78691deabe0e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -3758,7 +3758,7 @@
"http" "http"
], ],
"support": { "support": {
"source": "https://github.com/symfony/http-client/tree/v8.0.5" "source": "https://github.com/symfony/http-client/tree/v8.0.8"
}, },
"funding": [ "funding": [
{ {
@@ -3778,7 +3778,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-01-27T16:18:07+00:00" "time": "2026-03-30T15:14:47+00:00"
}, },
{ {
"name": "symfony/http-client-contracts", "name": "symfony/http-client-contracts",
+1 -1
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%'
-35
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');
}
}
-31
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');
}
}
-31
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');
}
}
-31
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');
}
}
-31
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');
}
}
-33
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');
}
}
-49
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');
}
}
-31
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');
}
}
-31
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');
}
}
-35
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');
}
}
-35
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');
}
}
@@ -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 Version20260409142022 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)');
} }
+60 -60
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;
@@ -37,16 +38,6 @@ class UserController extends AbstractController
* @throws TransportExceptionInterface * @throws TransportExceptionInterface
*/ */
#[Route('/user/{email}', name: 'app_user_by_mail', methods: ['GET'])]
public function index(string $email, KazApiService $apiKazService): Response
{
$user = $apiKazService->getUserData($email);
return $this->render('user/profil_infos.html.twig', [
'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(
maurine marked this conversation as resolved Outdated
Outdated
Review

On va pouvoir regarder comment fonctionne l'api :) et réactiver cette route

On va pouvoir regarder comment fonctionne l'api :) et réactiver cette route
Outdated
Review

tout à fait ! pour l'instant, je l'ai ignorée volontairement et je me suis basée sur mes fixtures pour vérifier que ma mise en page était OK

tout à fait ! pour l'instant, je l'ai ignorée volontairement et je me suis basée sur mes fixtures pour vérifier que ma mise en page était OK
@@ -54,67 +45,73 @@ 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);
try {
// Récupération des données de l'utilisateur sur l'API grâce à son email
$kazUser = $apiKazService->getUserData($user->getEmail()); $kazUser = $apiKazService->getUserData($user->getEmail());
// Initialisation de la variable $userData
$user = $user->updateFromKazUser($kazUser); $user = $user->updateFromKazUser($kazUser);
} catch (Exception $e) {
$this->addFlash('error', 'Impossible de charger vos données.');
return $this->render('user/index.html.twig', [
'form' => $this->createForm(UserProfileType::class, $user)->createView(),
'userData' => $user,
'isEditMode' => false,
], new Response(status: 422));
}
//TODO: modifier pour que ça communique avec l'API */ // Création du formulaire lié à l'utilisateur connecté
# 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);
melvin-leveque marked this conversation as resolved Outdated
Outdated
Review

Le formulaire permet seulement de modifier l'image ?

Le formulaire permet seulement de modifier l'image ?
Outdated
Review

normalement, non. Je peux modifier l'image, le numéro de téléphone et l'adresse mail de secours

normalement, non. Je peux modifier l'image, le numéro de téléphone et l'adresse mail de secours
# 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);
} }
// --- Fin gestion de l'image de profil ---
$alternateEmail = $form->get('alternateEmail')->getData(); // Synchronisation des données avec l'API
$regexEmail = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/'; $kazUser = $user->convertToKazUser();
if(isset($alternateEmail) && preg_match($regexEmail, $alternateEmail)) {
$user->setAlternateEmail($form->get('alternateEmail')->getData()); try {
} else { $apiKazService->updateUserData($user->getEmail(), $kazUser);
$alternateEmail->addError(new FormError('L\'adresse e-mail n\'est pas valide.')); } catch (Exception $e) {
$this->addFlash('error', 'Impossible de mettre à jour votre profil.');
} }
// Sauvegarde en base de données
$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
$response = new Response(
status: ($form->isSubmitted() && !$form->isValid()) ? 422 : 200
);
# 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,
], $response);
} }
#[Route('/mot-de-passe', name: 'app_user_edit_password', methods: ['GET', 'POST'])] #[Route('/mot-de-passe', name: 'app_user_edit_password', methods: ['GET', 'POST'])]
2
@@ -124,38 +121,41 @@ class UserController extends AbstractController
EntityManagerInterface $entityManager EntityManagerInterface $entityManager
): Response ): Response
{ {
# Création du formulaire // Création du formulaire
melvin-leveque marked this conversation as resolved Outdated
Outdated
Review

Très bien de vérifier l'ancien mot de passe

Très bien de vérifier l'ancien mot de passe
$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');
} }
} }
$response = new Response(
status: ($form->isSubmitted() && !$form->isValid()) ? 422 : 200
);
return $this->render('user/edit_password.html.twig', [ return $this->render('user/edit_password.html.twig', [
'form' => $form->createView(), 'form' => $form->createView(),
]); ], $response);
} }
} }
+31 -33
View File
@@ -10,35 +10,36 @@ use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class AppFixtures extends Fixture class AppFixtures extends Fixture
{ {
# Initialisation de l'outil de hachage de Symfony // Initialisation de l'outil de hachage de Symfony
private UserPasswordHasherInterface $hasher; private UserPasswordHasherInterface $hasher;
# Injection de dépendance pour récupérer le service de sécurité // Injection de dépendance pour récupérer le service de sécurité
public function __construct(UserPasswordHasherInterface $hasher) public function __construct(UserPasswordHasherInterface $hasher)
{ {
$this->hasher = $hasher; $this->hasher = $hasher;
} }
# Méthode principale qui génère les données en base // Méthode principale qui génère des données de test en BDD locale
public function load(ObjectManager $manager): void public function load(ObjectManager $manager): void
{ {
# Initialisation de Faker en français // --- Création de 10 utilisateurs avec Faker --- //
// Initialisation de Faker en français
$faker = Factory::create('fr_FR'); $faker = Factory::create('fr_FR');
# Boucle pour créer 10 utilisateurs // Boucle pour créer 10 utilisateurs
melvin-leveque marked this conversation as resolved Outdated
Outdated
Review

Niquel les fixtures :)

Niquel les fixtures :)
for ($i = 0; $i < 10; $i++) { for ($i = 0; $i < 10; $i++) {
# Instanciation d'un nouvel utilisateur (Adhérent) // Instanciation d'un nouvel utilisateur (Adhérent)
$user = new User(); $user = new User();
# Attribution d'un email aléatoire et unique // Attribution d'un email aléatoire et unique
$user->setEmail($faker->unique()->safeEmail()); $user->setEmail($faker->unique()->safeEmail());
# Définition des droits d'accès de l'utilisateur // Définition des droits d'accès de l'utilisateur
$user->setRoles(['ROLE_USER', 'ROLE_ORGANISATION']); $user->setRoles(['ROLE_USER', 'ROLE_ORGANISATION']);
# Hachage sécurisé du mot de passe "password" // Hachage sécurisé du mot de passe "password"
$user->setPassword($this->hasher->hashPassword($user, 'password')); $user->setPassword($this->hasher->hashPassword($user, 'password'));
# Définition d'un NOM et Prénom // Définition d'un NOM et Prénom
$user->setFirstname($faker->firstName()); $user->setFirstname($faker->firstName());
$user->setLastname($faker->lastName()); $user->setLastname($faker->lastName());
# autres fixtures à modifier plus tard // autres fixtures à modifier plus tard
$user->setNextcloudQuota($faker->numberBetween(1, 20) . 'G'); $user->setNextcloudQuota($faker->numberBetween(1, 20) . 'G');
$user->setQuota($faker->numberBetween(1, 10) . 'G'); $user->setQuota($faker->numberBetween(1, 10) . 'G');
$user->setEmailQuota('1G'); $user->setEmailQuota('1G');
@@ -47,7 +48,7 @@ class AppFixtures extends Fixture
$user->setHasMobilizon($faker->boolean(50)); $user->setHasMobilizon($faker->boolean(50));
$user->setHasNextcloudAccess($faker->boolean(90)); $user->setHasNextcloudAccess($faker->boolean(90));
$user->setIdentifiantKaz($faker->uuid()); $user->setIdentifiantKaz($faker->uuid());
# Préparation de l'enregistrement de l'objet en base de données // Préparation de l'enregistrement de l'objet en base de données
$manager->persist($user); $manager->persist($user);
} }
@@ -56,10 +57,9 @@ class AppFixtures extends Fixture
$admin->setEmail('admin@kaz.bzh'); $admin->setEmail('admin@kaz.bzh');
$admin->setRoles(['ROLE_USER', 'ROLE_ADMIN', 'ROLE_ORGANISATION']); $admin->setRoles(['ROLE_USER', 'ROLE_ADMIN', 'ROLE_ORGANISATION']);
$admin->setPassword($this->hasher->hashPassword($admin, 'password')); $admin->setPassword($this->hasher->hashPassword($admin, 'password'));
// Remplissage des champs obligatoires restants pour éviter les erreurs SQL
$admin->setFirstName('Admin'); $admin->setFirstName('Admin');
$admin->setLastName('KAZ'); $admin->setLastName('KAZ');
// Remplissage des champs obligatoires restants pour éviter les erreurs SQL
$admin->setAlternateEmail('secours@kaz.bzh'); $admin->setAlternateEmail('secours@kaz.bzh');
$admin->setIdentifiantKaz('ADMIN-KAZ-001'); $admin->setIdentifiantKaz('ADMIN-KAZ-001');
$admin->setQuota('5G'); $admin->setQuota('5G');
@@ -71,26 +71,24 @@ class AppFixtures extends Fixture
$manager->persist($admin); $manager->persist($admin);
// Création d'un compte de test fixe // Création d'un compte de test fixe présent dans le LDAP pour ma présentation
$melvin = new User(); $toto = new User();
$melvin->setEmail('melvin.leveque@kazkouil.fr'); $toto->setEmail('toto@kazkouil.fr');
$melvin->setRoles(['ROLE_USER', 'ROLE_ADMIN', 'ROLE_ORGANISATION']); $toto->setRoles(['ROLE_USER', 'ROLE_ADMIN', 'ROLE_ORGANISATION']);
$melvin->setPassword($this->hasher->hashPassword($melvin, 'password')); $toto->setPassword($this->hasher->hashPassword($toto, 'password'));
$melvin->setFirstName(''); $toto->setFirstName('');
$melvin->setLastName(''); $toto->setLastName('');
$toto->setAlternateEmail('');
$toto->setIdentifiantKaz('');
$toto->setQuota('5G');
$toto->setEmailQuota('1G');
$toto->setNextcloudQuota('10G');
$toto->setHasNextcloudAccess(true);
$toto->setHasMobilizon(true);
$toto->setHasAgoraAccess(true);
$manager->persist($toto);
$melvin->setAlternateEmail(''); // Exécution réelle des requêtes SQL (envoi vers la base), une fois la bouche finie
$melvin->setIdentifiantKaz('MELVIN-KAZ-001');
$melvin->setQuota('5G');
$melvin->setEmailQuota('1G');
$melvin->setNextcloudQuota('10G');
$melvin->setHasNextcloudAccess(true);
$melvin->setHasMobilizon(true);
$melvin->setHasAgoraAccess(true);
$manager->persist($melvin);
# Exécution réelle des requêtes SQL (envoi vers la base), une fois la bouche finie
$manager->flush(); $manager->flush();
} }
} }
+79 -13
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;
4
@@ -69,13 +68,18 @@ 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;
private ?string $numeroMembre = null;
private ?bool $mailEnabled = null;
private ?string $mailAlias = null;
public function __construct() { public function __construct() {
$this->emailQuota = self::EMAIL_QUOTA_DEFAULT; $this->emailQuota = self::EMAIL_QUOTA_DEFAULT;
} }
@@ -187,7 +191,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this->alternateEmail; return $this->alternateEmail;
} }
public function setAlternateEmail(string $alternateEmail): static public function setAlternateEmail(?string $alternateEmail): static
{ {
$this->alternateEmail = $alternateEmail; $this->alternateEmail = $alternateEmail;
@@ -290,14 +294,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,18 +318,80 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this; return $this;
} }
public function getNumeroMembre(): ?string
{
return $this->numeroMembre;
}
public function setNumeroMembre(?string $numeroMembre): static
{
$this->numeroMembre = $numeroMembre;
return $this;
}
public function isMailEnabled(): ?bool
{
return $this->mailEnabled;
}
public function setMailEnabled(?bool $mailEnabled): static
{
$this->mailEnabled = $mailEnabled;
return $this;
}
public function getMailAlias(): ?string
{
return $this->mailAlias;
}
public function setMailAlias(?string $mailAlias): static
{
$this->mailAlias = $mailAlias;
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']);
return $this; return $this;
} }
// Fonction qui permet de convertir les données de l'API vers $kazUser
public function convertToKazUser() : array
{
$data = [
'numeroMembre' => $this->getNumeroMembre(),
'mailDeSecours' => $this->getAlternateEmail(),
'mailEnabled' => $this->isMailEnabled(),
'nextcloudEnabled' => $this->hasNextcloudAccess(),
'mobilizonEnabled' => $this->hasMobilizon(),
'agoraEnabled' => $this->hasAgoraAccess(),
'identifiantKaz' => $this->getIdentifiantKaz(),
'mailAlias' => $this->getMailAlias(),
'quota' => $this->getQuota(),
];
return array_filter($data, fn($value) => $value !== null);
}
} }
+30 -20
View File
@@ -3,15 +3,17 @@
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;
use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Regex; use Symfony\Component\Validator\Constraints\Regex;
class UserProfileType extends AbstractType class UserProfileType extends AbstractType
@@ -19,30 +21,38 @@ class UserProfileType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
$builder $builder
->add('firstName', TextType::class, [ // Champs en lecture seule (données issues du LDAP)
'label' => 'Prénom', ->add('firstName', TextType::class, ['label' => 'Prénom', 'disabled' => true,])
'disabled' => true, ->add('lastName', TextType::class, ['label' => 'Nom', 'disabled' => true,])
->add('identifiantKaz', TextType::class, ['label' => 'Identifiant Kaz : ', 'disabled' => true,])
->add('email', EmailType::class, ['label' => 'E-mail', 'disabled' => true,])
->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,])
// Champs modifiables par l'adhérent
->add('alternateEmail', EmailType::class, [
'label' => 'E-mail de secours',
'constraints' => [
new NotBlank(message: 'L\'adresse e-mail de secours est obligatoire.'),
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('lastName', TextType::class, [
'label' => 'Nom',
'disabled' => true,
])
->add('email', EmailType::class, [
'label' => 'E-mail',
'disabled' => true,
])
->add('alternateEmail', EmailType::class, ['label' => 'E-mail de secours'])
->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,14 +61,14 @@ 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' => [
new Image( new Image(
maxSize: '2M', maxSize: '8M',
extensions: ['jpg', 'jpeg', 'png'], extensions: ['jpg', 'jpeg', 'png', 'gif'],
extensionsMessage: 'Veuillez déposer une image JPG, JPEG ou PNG valide',) extensionsMessage: 'Veuillez déposer une image JPG, JPEG, GIF ou PNG valide')
], ],
]) ])
; ;
+41 -10
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;
}
$filePath = rtrim($this->getTargetDirectory(), '/') . '/' . $fileName;
if (file_exists($filePath)) { if (file_exists($filePath)) {
unlink($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;
+25 -1
View File
@@ -4,6 +4,8 @@ namespace App\Service;
use Exception; use Exception;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\Multipart\FormDataPart;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
@@ -50,7 +52,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;
} }
@@ -80,4 +82,26 @@ class KazApiService
return $response->toArray(); return $response->toArray();
} }
/**
* Envoie les nouvelles données saisies par l'utilisateur vers l'API
*
* @throws ClientExceptionInterface
* @throws DecodingExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
* @throws Exception
*/
public function updateUserData(string $email, array $kazUser): void
{
$options['headers']['Authorization'] = 'Bearer ' . $this->getToken();
$options['headers']['Content-Type'] = 'application/json';
$options['json'] = $kazUser;
$response = $this->kazApiClient->request('PATCH', "/ldap/user/update/$email", $options);
if ($response->getStatusCode() !== 200) {
throw new Exception('Erreur lors de l\'appel API : ' . $response->getStatusCode());
}
}
} }
+52 -26
View File
@@ -1,31 +1,57 @@
<nav class="bg-white border-b border-gris-clair shadow-sm py-4 px-6 sticky top-0 z-50 font-sora"> {# Barre de navigation principale #}
melvin-leveque marked this conversation as resolved Outdated
Outdated
Review

Niquel les templates et le code front :)

Niquel les templates et le code front :)
<nav class="bg-white border-b border-gris-clair shadow-sm sticky top-0 z-50 font-sora relative">
<div class="max-w-7xl mx-auto flex flex-col md:flex-row items-center justify-between gap-4"> {# --- Gestion de la barre --- #}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex flex-col md:flex-row items-center gap-4 md:gap-8 w-full md:w-auto"> <div class="flex justify-between items-center h-20">
{# Logo de l'association #}
<a href="{{ path('app_home') }}" class="flex items-center gap-2 transition-transform hover:scale-105"> <a href="{{ path('app_home') }}"
<img src="{{ asset('img/logo.svg') }}" alt="Logo de l'association" class="h-10 w-auto object-contain"> class="flex-shrink-0 md:mr-8 flex items-center gap-2 transition-transform hover:scale-105">
<img src="{{ asset('img/logo.svg') }}" alt="Logo de l'association Kaz"
class="h-10 w-auto object-contain">
</a> </a>
<ul class="flex flex-wrap justify-center md:justify-start gap-2 md:gap-4"> {# Bouton Hamburger (Visible uniquement sur mobile) #}
<div class="md:hidden">
<button type="button" id="mobile-menu-button"
class="p-2 rounded-md text-gris-fonce hover:text-bouton focus:outline-none transition-colors"
aria-expanded="false">
<span class="sr-only">Ouvrir le menu</span>
{# Icône Menu (Hamburger) #}
<svg id="icon-menu-closed" class="block h-7 w-7" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"/>
</svg>
{# Icône Fermer (Croix) #}
<svg id="icon-menu-open" class="hidden h-7 w-7" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
{# --- Gestion des onglets --- #}
<div id="main-menu"
class="hidden md:flex md:flex-1 absolute md:static top-full left-0 w-full md:w-auto bg-white md:bg-transparent border-b md:border-none border-gris-clair shadow-lg md:shadow-none px-4 md:px-0 py-4 md:py-0">
{# Gestion de leur disposition horizontale ou verticale #}
<ul class="flex flex-col md:flex-row md:items-center gap-2 md:gap-4 lg:gap-8 w-full">
{# Onglet : Mon profil #} {# Onglet : Mon profil #}
<li> <li>
<a href="{{ path('app_user') }}" <a href="{{ path('app_user') }}"
class="px-4 py-2 text-sm font-semibold transition-colors block class="block px-4 py-3 md:py-2 text-base md:text-sm font-semibold transition-colors
{{ app.request.attributes.get('_route') == 'app_user' {{ app.request.attributes.get('_route') == 'app_user'
? 'bg-bouton/20 border border-bouton text-text rounded-lg' ? 'bg-bouton/20 text-text md:border md:border-bouton md:rounded-lg'
melvin-leveque marked this conversation as resolved Outdated
Outdated
Review

Top de conditionner l'affichage selon le rôle de l'utilisateur :)

Top de conditionner l'affichage selon le rôle de l'utilisateur :)
: 'text-gris-fonce hover:bg-gris-clair hover:text-text rounded-lg border border-transparent' }}"> : 'text-gris-fonce hover:bg-bg-primaire md:hover:bg-gris-clair md:hover:text-text rounded-lg md:border md:border-transparent' }}">
Mon profil Mon profil
</a> </a>
</li> </li>
{# Onglet : Mon offre #} {# Onglet : Mon offre #}
<li> <li>
{# TODO : créer la route {{ path('app_offres') }} #} <a href="https://kaz.bzh/offres/" target="_blank"
<a href="#" class="block px-4 py-3 md:py-2 text-base md:text-sm font-semibold text-gris-fonce hover:bg-bg-primaire md:hover:bg-gris-clair md:hover:text-text rounded-lg transition-colors md:border md:border-transparent">
class="px-4 py-2 text-sm font-semibold transition-colors block text-gris-fonce hover:bg-gris-clair hover:text-text rounded-lg border border-transparent flex items-center gap-1">
Mon offre Mon offre
</a> </a>
</li> </li>
@@ -33,33 +59,33 @@
{# Onglet : Gérer mes mots de passe #} {# Onglet : Gérer mes mots de passe #}
<li> <li>
<a href="{{ path('app_user_edit_password') }}" <a href="{{ path('app_user_edit_password') }}"
class="px-4 py-2 text-sm font-semibold transition-colors block class="block px-4 py-3 md:py-2 text-base md:text-sm font-semibold transition-colors
{{ app.request.attributes.get('_route') == 'app_user_edit_password' {{ app.request.attributes.get('_route') == 'app_user_edit_password'
? 'bg-bouton/20 border border-bouton text-text rounded-lg' ? 'bg-bouton/20 text-text md:border md:border-bouton md:rounded-lg'
: 'text-gris-fonce hover:bg-gris-clair hover:text-text rounded-lg border border-transparent' }}"> : 'text-gris-fonce hover:bg-bg-primaire md:hover:bg-gris-clair md:hover:text-text rounded-lg md:border md:border-transparent' }}">
Gérer mes mots de passe Mes mots de passe
</a> </a>
</li> </li>
{# Onglet : Mon organisation (ne s'affiche que si on a le rôle adéquat) #} {# Onglet : Mon organisation (Conditionnel) #}
{% if is_granted('ROLE_ADMIN_ORGANISATION') %} {% if is_granted('ROLE_ADMIN_ORGANISATION') %}
<li> <li>
<a href="#" <a href="#"
class="px-4 py-2 text-sm font-semibold transition-colors block text-gris-fonce hover:bg-gris-clair hover:text-text rounded-lg border border-transparent"> class="block px-4 py-3 md:py-2 text-base md:text-sm font-semibold text-gris-fonce hover:bg-bg-primaire md:hover:bg-gris-clair md:hover:text-text rounded-lg transition-colors md:border md:border-transparent">
Mon organisation Mon organisation
</a> </a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
</div>
<div class="flex-shrink-0 mt-4 md:mt-0"> {# Bouton de déconnexion #}
<div class="pt-4 md:pt-0 mt-2 md:mt-0 border-t md:border-t-0 border-gris-clair">
<a href="{{ path('app_logout') }}" <a href="{{ path('app_logout') }}"
class="px-4 py-2 text-sm font-bold bg-danger text-white rounded-lg hover:bg-danger-hover transition-colors shadow flex items-center gap-2"> class="flex items-center justify-center whitespace-nowrap w-full md:w-auto px-5 py-3 md:py-2 text-base md:text-sm font-bold bg-danger text-white rounded-lg hover:bg-danger-hover transition-all active:scale-95 shadow-sm">
Se déconnecter Se déconnecter
</a> </a>
</div> </div>
</div>
</div>
</div> </div>
</nav> </nav>
+41 -51
View File
@@ -3,7 +3,6 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{% block title %}Association Kaz{% endblock %}</title> <title>{% block title %}Association Kaz{% endblock %}</title>
<link rel="icon" href="{{ asset("img/logo.svg") }}"> <link rel="icon" href="{{ asset("img/logo.svg") }}">
@@ -12,83 +11,74 @@
{% endblock %} {% endblock %}
{% block javascripts %} {% block javascripts %}
{% block importmap %}{{ importmap('app') }}{% endblock %} {{ importmap('app') }}
{% endblock %} {% endblock %}
</head> </head>
<body> <body class="min-h-screen flex flex-col font-sora antialiased text-text bg-bg-primaire">
{% block navbar %}
{{ include('_navbar.html.twig') }} {{ include('_navbar.html.twig') }}
{% endblock %}
{# Contenu principal #} {# Contenu principal #}
<main class="flex-grow"> <main class="flex-grow">
{# Gestion du responsive et l'espacement pour toutes les pages #}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{# Affichage des messages flash (Succès ou Erreur) #} {# Affichage des messages flash (Succès ou Erreur) #}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
{% for label, messages in app.flashes %} {% for label, messages in app.flashes %}
{% for message in messages %} {% for message in messages %}
<div class="flex items-center p-4 mb-4 rounded-lg border shadow-sm {# Réglages des couleurs que prendront les messages en fonction de leur(s) paramètre(s) #}
{{ label == 'success' ? 'bg-green-50 border-green-200 text-green-800' : {% set colorClasses = {
label == 'error' ? 'bg-red-50 border-red-200 text-red-800' : 'success': 'bg-green-50 border-green-200 text-green-800',
label == 'warning' ? 'bg-yellow-50 border-yellow-200 text-yellow-800' : 'error': 'bg-red-50 border-red-200 text-red-800',
'bg-blue-50 border-blue-200 text-blue-800' }}" 'warning': 'bg-yellow-50 border-yellow-200 text-yellow-800',
'info': 'bg-blue-50 border-blue-200 text-blue-800'
} %}
{# Affichage du message de la couleur définie par sa fonction #}
<div
class="flex items-center p-4 mb-4 rounded-lg border shadow-sm {{ colorClasses[label] ?? colorClasses['info'] }}"
role="alert"> role="alert">
<div class="text-sm font-semibold flex-grow">
{# Icône dynamique #}
<svg class="flex-shrink-0 w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
{% if label == 'success' %}
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
{% else %}
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
{% endif %}
</svg>
<div class="text-sm font-bold">
{{ message }} {{ message }}
</div> </div>
{# Bouton Fermer #} {# Affichage du boutton "x" pour fermer le message flash #}
<button type="button" <button type="button"
onclick="this.parentElement.remove()" onclick="this.parentElement.remove()"
class="text-lg font-bold hover:opacity-70 transition-opacity ml-4"> class="ml-4 hover:opacity-50 transition-opacity"
&times; aria-label="Fermer">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button> </button>
</div> </div>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
</div> {# Affichage du body spécifique à chaque page #}
{# Contenu principal de chaque page #}
{% block body %}{% endblock %} {% block body %}{% endblock %}
</div>
</main> </main>
{# Gestion du pied-de-page du site #}
<footer class="bg-white border-t border-gris-clair py-6 sm:py-8 mt-auto w-full font-sora">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col md:flex-row justify-between items-center gap-4">
<div class="text-sm text-gris-fonce flex items-center gap-2 justify-center md:justify-start">
{# Logo de l'association #}
<img src="{{ asset('img/logo.svg') }}"
alt="Logo de l'association"
class="h-6 w-auto object-contain opacity-80 hover:opacity-100 transition-opacity">
{# Le texte et les liens #} {# Gestion du pied-de-page #}
<span> <footer class="w-full bg-white border-t border-gris-clair mt-auto py-6">
&copy; {{ 'now'|date('Y') }} | Kaz, le numérique sobre, libre, éthique et local. <div
</span> class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col md:flex-row justify-between items-center gap-6">
<div class="flex items-center gap-3 text-sm text-gris-fonce text-center md:text-left">
<img src="{{ asset('img/logo.svg') }}" alt="Logo" class="h-6 w-auto opacity-80">
<p>&copy; {{ 'now'|date('Y') }} — Kaz. Le numérique sobre, libre, éthique et local.</p>
</div> </div>
<ul class="flex flex-wrap justify-center gap-4 sm:gap-6 text-sm text-gris-fonce"> <nav>
<li> <ul class="flex flex-wrap justify-center gap-6 text-sm text-gris-fonce">
<a href="https://kaz.bzh/mentions-legales/" class="hover:text-bouton transition-colors">Mentions légales et statuts</a> <li><a href="https://kaz.bzh/mentions-legales/" target="_blank"
</li> class="hover:text-bouton transition-colors">Mentions légales</a></li>
<li> <li><a href="https://status.kaz.bzh/status/kaz" target="_blank"
<a href="https://status.kaz.bzh/status/kaz" class="hover:text-bouton transition-colors">Santé des services Kaz</a> class="hover:text-bouton transition-colors">État des services</a></li>
</li> <li><a href="https://kaz.bzh/contact/" target="_blank"
<li> class="hover:text-bouton transition-colors">Contact</a></li>
<a href="https://kaz.bzh/contact/" class="hover:text-bouton transition-colors">Contact</a>
</li>
</ul> </ul>
</nav>
</div> </div>
</footer> </footer>
</body> </body>
+10 -10
View File
@@ -3,7 +3,7 @@
{% block title %}Accueil | {{ parent() }}{% endblock %} {% block title %}Accueil | {{ parent() }}{% endblock %}
{% block body %} {% block body %}
<div class="min-h-screen bg-bg-primaire py-8 w-full font-sora"> <div class="py-8 w-full">
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
{# Bloc "message d'accueil" #} {# Bloc "message d'accueil" #}
@@ -16,23 +16,23 @@
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 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 #}
melvin-leveque marked this conversation as resolved Outdated
Outdated
Review

Bonne pratique de mettre les TODO dans le code :)

Bonne pratique de mettre les TODO dans le code :)
<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 : Ton abonnement actuellement :
</h3> </h3>
<ul class="space-y-2 text-sm text-text"> <ul class="space-y-2 text-sm text-text">
<li class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2"> <li class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
<span class="font-semibold text-gris-fonce">Formule souscrite :</span> <span class="font-semibold text-gris-fonce">Formule souscrite :</span>
{# TODO API : Remplacer par la vraie variable #} {# TODO API : Remplacer par la vraie variable quand connexion Pahéko OK #}
<span class="italic opacity-70">Ajouter la vraie valeur</span> <span class="italic opacity-70">Ici s'affichera la donnée récupérée grâce à l'API</span>
</li> </li>
<li class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2"> <li class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
<span class="font-semibold text-gris-fonce">Date de validité :</span> <span class="font-semibold text-gris-fonce">Date de validité :</span>
{# TODO API : Remplacer par la vraie variable #} {# TODO API : Remplacer par la vraie variable quand connexion Pahéko OK #}
<span class="italic opacity-70">Ajouter la vraie valeur</span> <span class="italic opacity-70">Ici s'affichera la donnée récupérée grâce à l'API</span>
</li> </li>
</ul> </ul>
</div> </div>
@@ -63,7 +63,7 @@
</div> </div>
<div class="ml-4"> <div class="ml-4">
<h3 class="text-lg font-semibold text-title group-hover:text-bouton transition-colors">Mon Offre</h3> <h3 class="text-lg font-semibold text-title group-hover:text-bouton transition-colors">Mon Offre</h3>
<p class="text-sm text-gris-fonce">Gérer mon adhésion KAZ</p> <p class="text-sm text-gris-fonce">Gérer mon adhésion Kaz</p>
</div> </div>
</a> </a>
@@ -91,7 +91,7 @@
</div> </div>
</a> </a>
{% else %} {% else %}
<a href="https://kaz.bzh/contact/" class="group flex items-center p-5 bg-white border border-gris-clair rounded-xl shadow-sm hover:shadow-md hover:border-bouton transition-all duration-200"> <a href="https://kaz.bzh/contact/" target="_blank" class="group flex items-center p-5 bg-white border border-gris-clair rounded-xl shadow-sm hover:shadow-md hover:border-bouton transition-all duration-200">
<div class="flex-shrink-0 bg-purple-50 text-purple-600 rounded-lg p-3 group-hover:bg-purple-600 group-hover:text-white transition-colors"> <div class="flex-shrink-0 bg-purple-50 text-purple-600 rounded-lg p-3 group-hover:bg-purple-600 group-hover:text-white transition-colors">
<span class="text-2xl block">✉️</span> <span class="text-2xl block">✉️</span>
</div> </div>
+1 -1
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page de connexion | Association KAZ</title> <title>Page de connexion | Association Kaz</title>
{% block importmap %}{{ importmap('app') }}{% endblock %} {% block importmap %}{{ importmap('app') }}{% endblock %}
</head> </head>
2
+37 -25
View File
@@ -3,72 +3,84 @@
{% block title %}Modifier mes mots de passe | {{ parent() }}{% endblock %} {% block title %}Modifier mes mots de passe | {{ parent() }}{% endblock %}
{% block body %} {% block body %}
<div class="min-h-screen bg-bg-primaire flex items-center justify-center p-4 font-sora"> <div class="max-w-md mx-auto w-full bg-white rounded-2xl shadow-xl p-8 border-t-4 border-bouton mt-10 md:mt-20">
<div class="max-w-md w-full bg-white rounded-2xl shadow-xl p-8 border-t-4 border-bouton">
<h1 class="font-caveat text-4xl text-text mb-6 text-center"> <h1 class="font-caveat text-4xl text-text mb-12 text-center">
Sécurité du compte Modifier le mot de passe de mon espace kaznaute
</h1> </h1>
{{ form_start(form) }} {{ form_start(form) }}
<div class="space-y-4"> <div class="space-y-6">
{# Champ Ancien Mot de Passe #} {# Champ Ancien Mot de Passe #}
<div class="space-y-1"> <div class="flex flex-col gap-1.5">
{{ form_label(form.oldPassword, 'Mot de passe actuel', { {{ form_label(form.oldPassword, 'Mon mot de passe actuel', {
'label_attr': {'class': 'block text-sm font-semibold text-text'} 'label_attr': {'class': 'block text-sm font-semibold text-text'}
}) }} }) }}
{{ form_widget(form.oldPassword, { {{ form_widget(form.oldPassword, {
'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'} '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-all',
'placeholder':'Saisissez votre ancien mot de passe'
}
}) }} }) }}
{# Affichage message pour les erreurs de saisie de l'ancien mot de passe #} {# Affichage message pour les erreurs de saisie de l'ancien mot de passe #}
{% if form_errors(form.oldPassword) %}
<div class="text-red-500 text-xs mt-1 italic"> <div class="text-red-500 text-xs mt-1 italic">
{{ form_errors(form.oldPassword) }} {{ form_errors(form.oldPassword) }}
</div> </div>
{% endif %}
</div> </div>
<hr class="border-gris-clair/50">
{# Champs Nouveau Mot de Passe #} {# Champs Nouveau Mot de Passe #}
<div class="space-y-4 pt-2"> <div class="space-y-6">
{# Affichage de l'erreur si les deux champs ne correspondent pas #} {# Affichage d'un message d'erreur si les deux champs ne correspondent pas #}
{% if form_errors(form.newPassword) %}
<div class="text-red-500 text-xs italic"> <div class="text-red-500 text-xs italic">
{{ form_errors(form.newPassword) }} {{ form_errors(form.newPassword) }}
</div> </div>
<div class="space-y-1"> {% endif %}
{{ form_label(form.newPassword.first, 'Nouveau mot de passe', {
<div class="flex flex-col gap-1.5">
{{ form_label(form.newPassword.first, 'Mon nouveau mot de passe', {
'label_attr': {'class': 'block text-sm font-semibold text-text'} 'label_attr': {'class': 'block text-sm font-semibold text-text'}
}) }} }) }}
{{ form_widget(form.newPassword.first, { {{ form_widget(form.newPassword.first, {
'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 transition-shadow'} '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-all',
'placeholder':'Saisissez un nouveau mot de passe'
}
}) }} }) }}
{% if form_errors(form.newPassword.first) %}
{# Affichage de l'erreur de longueur (min 8 caractères) #} <div class="text-red-500 text-xs italic">
<div class="text-red-500 text-xs mt-1 italic">
{{ form_errors(form.newPassword.first) }} {{ form_errors(form.newPassword.first) }}
</div> </div>
{% endif %}
</div> </div>
<div class="space-y-1"> <div class="flex flex-col gap-1.5">
{{ form_label(form.newPassword.second, 'Confirmer le nouveau mot de passe', { {{ form_label(form.newPassword.second, 'Confirmer mon nouveau mot de passe', {
'label_attr': {'class': 'block text-sm font-semibold text-text'} 'label_attr': {'class': 'block text-sm font-semibold text-text'}
}) }} }) }}
{{ form_widget(form.newPassword.second, { {{ form_widget(form.newPassword.second, {
'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 transition-shadow'} '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-all',
'placeholder': 'Confirmez votre saisie'
}
}) }} }) }}
</div> </div>
<div class="text-red-500 text-xs mt-1">
{{ form_errors(form.newPassword) }}
</div>
</div> </div>
{# Bouton de validation #} {# Bouton de validation #}
<div class="flex justify-center pt-12 pb-2">
<button type="submit" <button type="submit"
class="w-full bg-bouton hover:bg-bouton-hover text-text font-bold py-3 rounded-lg shadow-md transition-colors mt-6"> class="w-full sm:w-auto px-8 py-3 bg-bouton hover:bg-bouton-hover text-text text-sm font-bold rounded-lg shadow-md transition-all transform active:scale-95">
Mettre à jour mon mot de passe Mettre à jour mon mot de passe
</button> </button>
</div> </div>
</div>
{{ form_end(form) }} {{ form_end(form) }}
</div> </div>
</div>
{% endblock %} {% endblock %}
+224 -75
View File
@@ -1,135 +1,284 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}Accueil | {{ parent() }}{% endblock %} {# @var userData \App\Entity\User #}
{# @var form \Symfony\Component\Form\FormView #}
{# @var isEditMode bool #}
{% 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="py-4 w-full">
melvin-leveque marked this conversation as resolved Outdated
Outdated
Review

On peut aussi avoir l'information de la photo seulement au niveau de l'app symfony :)
C'est toujours sympa d'avoir de la personnalisation pour les utilisateurs

On peut aussi avoir l'information de la photo seulement au niveau de l'app symfony :) C'est toujours sympa d'avoir de la personnalisation pour les utilisateurs
Outdated
Review

d'ac !

d'ac !
{# Affichage du formulaire (seulement en mode édition) #}
{% if isEditMode %}
{{ form_start(form, {'attr': {'class': 'contents', 'novalidate': 'novalidate'}}) }}
{% endif %}
{{ 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'}}) }} {# Gestion des boutons d'action (Modifier / Annuler) et des titres #}
<div class="relative flex items-center justify-center mb-4">
<h1 class="text-4xl font-caveat text-text m-0">Mon profil</h1>
{# TODO: voir si c'est pertinent avec l'API et s'il y a l'utilité d'une photo de profil #} <div class="absolute right-0">
{# Gestion de la colone avec le choix de la photo de profil #} {% if isEditMode %}
<div class="flex flex-col text-text items-center"> <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">
{# Affichage de la photo de profil #} ❌ Annuler
<div class="w-full md:w-64 flex-shrink-0 mt-20"> </a>
<div class="border-2 border-black p-1 bg-white shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]">
{% if userData.photo %}
<img src="{{ asset('uploads/images/' ~ userData.photo) }}"
alt="Photo de profil"
class="w-full aspect-[4/3] object-cover">
{% else %} {% else %}
<div class="w-full aspect-[4/3] bg-gray-50 flex items-center justify-center text-6xl"> <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">
</div> ✏️ Modifier
</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<p class="text-2xl text-title font-caveat mt-4"> Ma photo</p> {# --- Gestion de l'affichage des informations personnelles et de la photo --- #}
{# Mise en page en colonne #}
<div class="grid md:grid-cols-3 gap-4">
{# Gestion du dépôt d'un fichier image #} {# Colonne Photo #}
<div class="flex flex-col text-text items-center">
<div class="flex-shrink-0">
{% if userData.image %}
{# Design avec photo #}
<img src="{{ asset('uploads/images/' ~ userData.image) }}"
alt="Photo de profil"
class="w-48 h-48 md:w-56 md:h-56 p-1 rounded-full ring-2 ring-gris-clair object-cover">
{% else %}
{# Design sans photo #}
<div
class="relative w-48 h-48 md:w-56 md:h-56 overflow-hidden bg-neutral-200 rounded-full flex items-end justify-center shadow-sm">
<svg class="w-5/6 h-5/6 text-neutral-500" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
clip-rule="evenodd"></path>
</svg>
</div>
{% endif %}
</div>
<p class="text-2xl text-title font-caveat mt-3"> Ma photo</p>
{# Gestion du dépôt d'un fichier image (Uniquement en mode édition) #}
{% if isEditMode %}
<div class="w-full mt-2"> <div class="w-full mt-2">
{{ form_label(form.image, 'Choisir un fichier', { {{ form_label(form.image, 'Choisir un fichier', {
'label_attr': {'class': 'block mb-2.5 text-sm font-medium text-gris-fonce'} 'label_attr': {'class': 'block mb-2.5 text-sm font-medium text-gris-fonce'}
}) }} }) }}
{{ form_widget(form.image, { {{ form_widget(form.image, {
'attr': { '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', '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',
'aria-describedby': 'file_input_help' 'aria-describedby': 'file_input_help'
} }
}) }} }) }}
<p class="mt-1 text-sm text-gris-moyen" id="file_input_help"> <p class="mt-1 text-sm text-gris-moyen" id="file_input_help">
JPG, JPEG ou PNG (Taille max : 2Mo). JPG, JPEG, GIF ou PNG (Taille max : 8Mo).
</p> </p>
</div>
</div>
{# 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"> <div class="text-red-500 text-xs mt-1 italic font-sora">
{{ form_errors(form.firstName) }} {{ form_errors(form.image) }}
</div> </div>
</div> </div>
{% endif %}
</div>
{# Colonne Infos Persos #}
<div class="md:col-span-2">
<h2 class="text-2xl font-caveat text-text mb-4 text-center">Mes informations personnelles</h2>
<div class="flex flex-col gap-4">
{# Identifiant #}
<div class="space-y-1"> <div class="space-y-1">
{{ form_label(form.lastName, 'Prénom :', { <label class="block text-base font-semibold text-text">
'label_attr': {'class': 'block text-sm font-semibold text-text'} Identifiant Kaz :
}) }} </label>
{{ form_widget(form.lastName, { {% if isEditMode %}
'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'} {{ form_widget(form.identifiantKaz, {'attr': {'class': 'w-full px-4 py-3 text-base bg-gray-100 border border-gris-clair rounded-lg text-gray-500 cursor-not-allowed'}}) }}
}) }} {% else %}
{# Implémentation d'un message d'errer en cas de problème #} <div
class="w-full px-4 py-3 text-base bg-gray-50 border border-gris-clair rounded-lg text-text">
{{ userData.identifiantKaz ?? 'Non défini' }}
</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-base font-semibold text-text">
NOM :
</label>
{% if isEditMode %}
{{ form_widget(form.lastName, {'attr': {'class': 'w-full px-4 py-3 text-base bg-gray-100 border border-gris-clair rounded-lg text-gray-500 cursor-not-allowed'}}) }}
<div class="text-red-500 text-xs mt-1 italic font-sora"> <div class="text-red-500 text-xs mt-1 italic font-sora">
{{ form_errors(form.lastName) }} {{ form_errors(form.lastName) }}
</div> </div>
{% else %}
<div
class="w-full px-4 py-3 text-base 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-base font-semibold text-text">
Prénom :
</label>
{% if isEditMode %}
{{ form_widget(form.firstName, {'attr': {'class': 'w-full px-4 py-3 text-base bg-gray-100 border border-gris-clair rounded-lg text-gray-500 cursor-not-allowed'}}) }}
<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 text-base bg-gray-50 border border-gris-clair rounded-lg text-text">
{{ userData.firstName }}
</div>
{% endif %}
</div> </div>
</div> </div>
{# Champ Téléphone #} {# Téléphone #}
<div class="space-y-1"> <div class="space-y-1">
{{ form_label(form.telephone, 'Numéro de téléphone', { <label class="block text-base font-semibold text-text">
'label_attr': {'class': 'block text-sm font-semibold text-text'} Numéro de téléphone :
}) }} </label>
{{ form_widget(form.telephone) }} {% if isEditMode %}
{# Implémentation d'un message d'errer en cas de problème #} {{ form_widget(form.telephone, {'attr': {'class': 'w-full px-4 py-3 text-base bg-white 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"> <div class="text-red-500 text-xs mt-1 italic">
{{ form_errors(form.telephone) }} {{ form_errors(form.telephone) }}
</div> </div>
{% else %}
<div
class="w-full px-4 py-3 text-base bg-gray-50 border border-gris-clair rounded-lg text-text">
{{ userData.telephone ?? 'Non renseigné' }}
</div>
{% endif %}
</div> </div>
{# Champ E-mail #} {# E-mail #}
<div class="space-y-1"> <div class="space-y-1">
{{ form_label(form.email, 'E-mail :', { <label class="block text-base font-semibold text-text">
'label_attr': {'class': 'block text-sm font-semibold text-text'} E-mail :
}) }} </label>
{{ form_widget(form.email, { {% if isEditMode %}
'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'} {{ form_widget(form.email, {'attr': {'class': 'w-full px-4 py-3 text-base bg-gray-100 border border-gris-clair rounded-lg text-gray-500 cursor-not-allowed'}}) }}
}) }}
{# Implémentation d'un message d'errer en cas de problème #}
<div class="text-red-500 text-xs mt-1 italic font-sora"> <div class="text-red-500 text-xs mt-1 italic font-sora">
{{ form_errors(form.email) }} {{ form_errors(form.email) }}
</div> </div>
{% else %}
<div
class="w-full px-4 py-3 text-base bg-gray-50 border border-gris-clair rounded-lg text-text">
{{ userData.email }}
</div>
{% endif %}
</div> </div>
{# Champ E-mail de secours #} {# E-mail de secours #}
<div class="space-y-1"> <div class="space-y-1">
{{ form_label(form.alternateEmail, 'E-mail de secours :', { <label class="block text-base font-semibold text-text">
'label_attr': {'class': 'block text-sm font-semibold text-text'} E-mail de secours :
}) }} </label>
{{ form_widget(form.alternateEmail, { {% if isEditMode %}
'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'} {{ form_widget(form.alternateEmail, {'attr': {'class': 'w-full px-4 py-3 text-base bg-white 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"> <div class="text-red-500 text-xs mt-1 italic font-sora">
{{ form_errors(form.alternateEmail) }} {{ form_errors(form.alternateEmail) }}
</div> </div>
{% else %}
<div
class="w-full px-4 py-3 text-base bg-gray-50 border border-gris-clair rounded-lg text-text">
{{ userData.alternateEmail ?? 'Non renseigné' }}
</div>
{% endif %}
</div> </div>
<div class="flex flex-col sm:flex-row gap-4 pt-2"> {# Quota Email #}
<div class="space-y-1">
<label class="block text-base font-semibold text-text">
Espace disponible dans votre boîte mail :
</label>
{% if isEditMode %}
{{ form_widget(form.emailQuota, {'attr': {'class': 'w-full px-4 py-3 text-base bg-gray-100 border border-gris-clair rounded-lg text-gray-500 cursor-not-allowed'}}) }}
{% else %}
<div
class="w-full px-4 py-3 text-base bg-gray-50 border border-gris-clair rounded-lg text-text">
{{ userData.emailQuota ?? 'Non défini' }}
</div>
{% endif %}
</div>
{# Quota Nextcloud #}
<div class="space-y-1 sm:col-span-2">
<label class="block text-base font-semibold text-text">
Quota Nextcloud :
</label>
{% if isEditMode %}
{{ form_widget(form.nextcloudQuota, {'attr': {'class': 'w-full px-4 py-3 text-base bg-gray-100 border border-gris-clair rounded-lg text-gray-500 cursor-not-allowed'}}) }}
{% else %}
<div
class="w-full px-4 py-3 text-base bg-gray-50 border border-gris-clair rounded-lg text-text">
{{ userData.nextcloudQuota ?? 'Non défini' }}
</div>
{% endif %}
</div>
{# --- Gestion de l'affichage des checkbox des différents accès --- #}
<div class="sm:col-span-2 grid grid-cols-1 sm:grid-cols-3 gap-4 pt-4 border-t border-gris-clair">
{# Accès Nextcloud #}
<div class="flex items-center gap-3">
{% if isEditMode %}
{{ form_widget(form.hasNextcloudAccess, {'attr': {'class': 'w-5 h-5 text-bouton border-gris-clair rounded focus:ring-bouton cursor-not-allowed opacity-60'}}) }}
{{ form_label(form.hasNextcloudAccess, null, {'label_attr': {'class': 'text-base font-medium text-text'}}) }}
{% else %}
<span class="text-l">{% if userData.hasNextcloudAccess %}{% else %}{% endif %}</span>
<span class="text-base font-medium text-text">Accès Nextcloud</span>
{% endif %}
</div>
{# Accès Mobilizon #}
<div class="flex items-center gap-3">
{% if isEditMode %}
{{ form_widget(form.hasMobilizon, {'attr': {'class': 'w-5 h-5 text-bouton border-gris-clair rounded focus:ring-bouton cursor-not-allowed opacity-60'}}) }}
{{ form_label(form.hasMobilizon, null, {'label_attr': {'class': 'text-base font-medium text-text'}}) }}
{% else %}
<span class="text-l">{% if userData.hasMobilizon %}{% else %}{% endif %}</span>
<span class="text-base font-medium text-text">Accès Mobilizon</span>
{% endif %}
</div>
{# Accès Agora #}
<div class="flex items-center gap-3">
{% if isEditMode %}
{{ form_widget(form.hasAgoraAccess, {'attr': {'class': 'w-5 h-5 text-bouton border-gris-clair rounded focus:ring-bouton cursor-not-allowed opacity-60'}}) }}
{{ form_label(form.hasAgoraAccess, null, {'label_attr': {'class': 'text-base font-medium text-text'}}) }}
{% else %}
<span class="text-l">{% if userData.hasAgoraAccess %}{% else %}{% endif %}</span>
<span class="text-base font-medium text-text">Accès Agora</span>
{% endif %}
</div>
</div>
{# Affichage du bouton "Valider" (seulement en mode édition) #}
{% if isEditMode %}
<div class="flex justify-center pt-4 pb-2">
<button type="submit" <button type="submit"
class="flex-1 py-3 bg-bouton hover:bg-bouton-hover text-text font-bold rounded-lg shadow transition-colors"> class="px-8 py-2.5 bg-bouton hover:bg-bouton-hover text-text text-sm font-bold rounded-lg shadow transition-colors">
Valider Enregistrer les modifications
</button> </button>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
{# Fermuture du formulaire (seulement en mode édition) #}
{% if isEditMode %}
{{ form_end(form) }} {{ form_end(form) }}
{% endif %}
</div>
</div> </div>
{% endblock %} {% endblock %}
-11
View File
@@ -1,11 +0,0 @@
{% extends 'base.html.twig' %}
{% block title %}Accueil | {{ parent() }}{% endblock %}
{% block body %}
<div class="min-h-screen bg-bg-primaire py-8 w-full font-sora">
<div>
<span>Identifiant : {{ user.identifiantKaz }}</span>
</div>
</div>
{% endblock %}
+2
View File
@@ -0,0 +1,2 @@
# translations/security.fr.yaml
"Invalid credentials.": "Identifiants invalides. Veuillez vérifier votre email ou votre mot de passe."