feat(security): Utilise l'email pour l'authentification, crée dashboard admin.

Ajoute le dashboard admin, les membres, les events, et les comptes.
Utilise l'email au lieu du username pour l'authentification.
```
This commit is contained in:
Serreau Jovann
2025-11-17 15:01:38 +01:00
parent 75f3533776
commit 749e47882a
17 changed files with 870 additions and 2 deletions

View File

@@ -8,7 +8,7 @@ security:
app_account_provider:
entity:
class: App\Entity\Account
property: username # Utilise le champ 'username' de votre entité Account pour l'authentification
property: email # Utilise le champ 'username' de votre entité Account pour l'authentification
firewalls:
dev:

View File

@@ -9,6 +9,14 @@ vich_uploader:
inject_on_load: true
delete_on_update: true
delete_on_remove: true
members:
uri_prefix: /storage/members
upload_destination: '%kernel.project_dir%/public/storage/members'
namer: App\VichUploader\Namer\Account\AvatarName # Replaced namer
directory_namer: App\VichUploader\DirectoryNamer\Account\AvatarName
inject_on_load: true
delete_on_update: true
delete_on_remove: true
#mappings:
# products:
# uri_prefix: /images/products

View File

@@ -0,0 +1,32 @@
<?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 Version20251117134224 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 members (id SERIAL NOT NULL, pseudo VARCHAR(255) NOT NULL, role VARCHAR(255) NOT NULL, cosplayer BOOLEAN NOT NULL, crosscosplayer BOOLEAN NOT NULL, trans BOOLEAN NOT NULL, orientation VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP TABLE members');
}
}

View File

@@ -0,0 +1,43 @@
<?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 Version20251117135736 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 members ADD member_file_name VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE members ADD member_dimensions JSON DEFAULT NULL');
$this->addSql('ALTER TABLE members ADD member_size VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE members ADD member_mine_type VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE members ADD member_original_name VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE members ADD update_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
$this->addSql('COMMENT ON COLUMN members.update_at IS \'(DC2Type:datetime_immutable)\'');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE members DROP member_file_name');
$this->addSql('ALTER TABLE members DROP member_dimensions');
$this->addSql('ALTER TABLE members DROP member_size');
$this->addSql('ALTER TABLE members DROP member_mine_type');
$this->addSql('ALTER TABLE members DROP member_original_name');
$this->addSql('ALTER TABLE members DROP update_at');
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace App\Controller\Admin;
use App\Entity\Account;
use App\Entity\AccountResetPasswordRequest;
use App\Entity\Members;
use App\Form\MembersType;
use App\Form\RequestPasswordConfirmType;
use App\Form\RequestPasswordRequestType;
use App\Repository\MembersRepository;
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
use App\Service\ResetPassword\Event\ResetPasswordEvent;
use Doctrine\ORM\EntityManagerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Twig\Environment;
class AdminController extends AbstractController
{
#[Route(path: '/admin', name: 'admin_dashboard', options: ['sitemap' => false], methods: ['GET'])]
public function adminDashboard(): Response
{
return $this->render('admin/dashboard.twig', [
]);
}
#[Route(path: '/admin/members', name: 'admin_members', options: ['sitemap' => false], methods: ['GET'])]
public function adminMembers(MembersRepository $membersRepository): Response
{
return $this->render('admin/members.twig', [
'members' => $membersRepository->findBy([],['id' => 'ASC']),
]);
}
#[Route(path: '/admin/members/{id}', name: 'admin_member_edit', options: ['sitemap' => false], methods: ['GET','POST'])]
public function adminMembersEdit(?Members $members,Request $request,EntityManagerInterface $entityManager): Response
{
$form = $this->createForm(MembersType::class, $members);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager->persist($members);
$entityManager->flush();
return $this->redirectToRoute('admin_members');
}
return $this->render('admin/member/add.twig', [
'form' => $form->createView(),
]);
}
#[Route(path: '/admin/members/add', name: 'admin_member_create', options: ['sitemap' => false], methods: ['GET','POST'], priority: 5)]
public function adminMembersCreate(Request $request,EntityManagerInterface $entityManager): Response
{
$members = new Members();
$members->setTrans(false);
$members->setCrosscosplayer(false);
$members->setCosplayer(false);
$form = $this->createForm(MembersType::class, $members);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager->persist($members);
$entityManager->flush();
return $this->redirectToRoute('admin_members');
}
return $this->render('admin/member/add.twig', [
'form' => $form->createView(),
]);
}
#[Route(path: '/admin/members/delete/{id}', name: 'admin_member_delete', options: ['sitemap' => false], methods: ['GET'])]
public function adminMembersDelete(): Response
{
}
#[Route(path: '/admin/events', name: 'admin_events', options: ['sitemap' => false], methods: ['GET'])]
public function adminEvents(): Response
{
return $this->render('admin/dashboard.twig', [
]);
}
#[Route(path: '/admin/account', name: 'admin_accounts_list', options: ['sitemap' => false], methods: ['GET'])]
public function adminAccount(): Response
{
return $this->render('admin/dashboard.twig', [
]);
}
}

View File

@@ -26,6 +26,8 @@ class SecurityController extends AbstractController
#[Route(path: '/connexion', name: 'app_login', options: ['sitemap' => false], methods: ['GET','POST'])]
public function login(AuthenticationUtils $authenticationUtils): Response
{
if($this->getUser())
return $this->redirectToRoute('app_home');
return $this->render('security/login.twig', [
'last_username' => $authenticationUtils->getLastUsername(),
'error' => $authenticationUtils->getLastAuthenticationError(),

257
src/Entity/Members.php Normal file
View File

@@ -0,0 +1,257 @@
<?php
namespace App\Entity;
use App\Repository\MembersRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
#[ORM\Entity(repositoryClass: MembersRepository::class)]
#[Vich\Uploadable()]
class Members
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $pseudo = null;
#[ORM\Column(length: 255)]
private ?string $role = null;
#[ORM\Column]
private ?bool $cosplayer = null;
#[ORM\Column]
private ?bool $crosscosplayer = null;
#[ORM\Column]
private ?bool $trans = null;
#[ORM\Column(length: 255)]
private ?string $orientation = null;
#[Vich\UploadableField(mapping: 'members',fileNameProperty: 'memberFileName', size: 'memberSize', mimeType: 'memberMineType', originalName: 'memberOriginalName',dimensions: 'memberDimensions')]
private ?File $members = null;
#[ORM\Column(nullable: true)]
private ?string $memberFileName = null;
#[ORM\Column(nullable: true)]
private ?array $memberDimensions = [];
#[ORM\Column(length: 255,nullable: true)]
private ?string $memberSize = null;
#[ORM\Column(length: 255,nullable: true)]
private ?string $memberMineType = null;
#[ORM\Column(length: 255,nullable: true)]
private ?string $memberOriginalName = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $updateAt;
public function getId(): ?int
{
return $this->id;
}
public function getPseudo(): ?string
{
return $this->pseudo;
}
public function setPseudo(string $pseudo): static
{
$this->pseudo = $pseudo;
return $this;
}
public function getRole(): ?string
{
return $this->role;
}
public function setRole(string $role): static
{
$this->role = $role;
return $this;
}
public function isCosplayer(): ?bool
{
return $this->cosplayer;
}
public function setCosplayer(bool $cosplayer): static
{
$this->cosplayer = $cosplayer;
return $this;
}
public function isCrosscosplayer(): ?bool
{
return $this->crosscosplayer;
}
public function setCrosscosplayer(bool $crosscosplayer): static
{
$this->crosscosplayer = $crosscosplayer;
return $this;
}
public function isTrans(): ?bool
{
return $this->trans;
}
public function setTrans(bool $trans): static
{
$this->trans = $trans;
return $this;
}
public function getOrientation(): ?string
{
return $this->orientation;
}
public function setOrientation(string $orientation): static
{
$this->orientation = $orientation;
return $this;
}
/**
* @return \DateTimeImmutable|null
*/
public function getUpdateAt(): ?\DateTimeImmutable
{
return $this->updateAt;
}
/**
* @return bool|null
*/
public function getCosplayer(): ?bool
{
return $this->cosplayer;
}
/**
* @return bool|null
*/
public function getCrosscosplayer(): ?bool
{
return $this->crosscosplayer;
}
/**
* @return array|null
*/
public function getMemberDimensions(): ?array
{
return $this->memberDimensions;
}
/**
* @return string|null
*/
public function getMemberFileName(): ?string
{
return $this->memberFileName;
}
/**
* @return string|null
*/
public function getMemberMineType(): ?string
{
return $this->memberMineType;
}
/**
* @return string|null
*/
public function getMemberOriginalName(): ?string
{
return $this->memberOriginalName;
}
/**
* @return File|null
*/
public function getMembers(): ?File
{
return $this->members;
}
/**
* @return string|null
*/
public function getMemberSize(): ?string
{
return $this->memberSize;
}
/**
* @param \DateTimeImmutable|null $updateAt
*/
public function setUpdateAt(?\DateTimeImmutable $updateAt): void
{
$this->updateAt = $updateAt;
}
/**
* @param array|null $memberDimensions
*/
public function setMemberDimensions(?array $memberDimensions): void
{
$this->memberDimensions = $memberDimensions;
}
/**
* @param File|null $members
*/
public function setMembers(?File $members): void
{
$this->members = $members;
}
/**
* @param string|null $memberFileName
*/
public function setMemberFileName(?string $memberFileName): void
{
$this->memberFileName = $memberFileName;
}
/**
* @param string|null $memberMineType
*/
public function setMemberMineType(?string $memberMineType): void
{
$this->memberMineType = $memberMineType;
}
/**
* @param string|null $memberOriginalName
*/
public function setMemberOriginalName(?string $memberOriginalName): void
{
$this->memberOriginalName = $memberOriginalName;
}
/**
* @param string|null $memberSize
*/
public function setMemberSize(?string $memberSize): void
{
$this->memberSize = $memberSize;
}
}

94
src/Form/MembersType.php Normal file
View File

@@ -0,0 +1,94 @@
<?php
namespace App\Form;
use App\Entity\Members;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class MembersType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('members', FileType::class, [
'label' => 'Photo du membre (Max 2Mo)',
'required' => false, // Rendre facultatif
'mapped' => false, // Indiquer que ce champ n'est pas directement mappé à une propriété de l'entité
'attr' => [
'placeholder' => 'Choisir un fichier...',
]
])
->add('pseudo', TextType::class, [
'label' => 'Pseudo du membre',
'required' => true,
])
->add('role', ChoiceType::class, [
'label' => 'Rôle au sein de l\'association',
'choices' => [
'Président(e)' => 'Président(e)',
'Trésorier(e)' => 'Trésorier(e)',
'Secrétaire(e)' => 'Secrétaire(e)',
'Vice-Président(e)' => 'Vice-Président(e)',
'Trésorier(e) Adjoints' => 'Trésorier(e) Adjoints',
'Secrétaire(e) Adjoints' => 'Secrétaire(e) Adjoints',
],
])
->add('orientation', ChoiceType::class, [
'label' => 'Orientation (Tag)',
'choices' => [
'Non spécifié' => 'not_specified',
'Asexuel(le)' => 'asexual',
'Bisexuel(le)' => 'bisexual',
'Demisexuel(le)' => 'demisexual',
'Gay' => 'gay',
'Hétérosexuel(le)' => 'heterosexual',
'Lesbienne' => 'lesbian',
'Pansexuel(le)' => 'pansexual',
'Queer' => 'queer',
'En questionnement' => 'questioning',
'Autre' => 'other',
],
// Optionnel : affichez-le comme une liste déroulante normale (pas expanded)
])
// Les champs booléens sont mieux affichés comme des boutons radio (expanded)
->add('crosscosplayer', ChoiceType::class, [
'label' => 'Crosscosplayer ?',
'choices' => [
'Non' => false,
'Oui' => true,
],
'expanded' => true, // Affiche comme boutons radio
'multiple' => false,
])
->add('trans', ChoiceType::class, [
'label' => 'Transgenre ?',
'choices' => [
'Non' => false,
'Oui' => true,
],
'expanded' => true,
'multiple' => false,
])
->add('cosplayer', ChoiceType::class, [
'label' => 'Cosplayer ?',
'choices' => [
'Non' => false,
'Oui' => true,
],
'expanded' => true,
'multiple' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class', Members::class);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\Members;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Members>
*/
class MembersRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Members::class);
}
// /**
// * @return Members[] Returns an array of Members objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('m')
// ->andWhere('m.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('m.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Members
// {
// return $this->createQueryBuilder('m')
// ->andWhere('m.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

129
templates/admin/base.twig Normal file
View File

@@ -0,0 +1,129 @@
{# Assurez-vous d'utiliser une version de Tailwind CSS qui supporte ces classes #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{# 🛑 NO INDEX DIRECTIVE #}
<meta name="robots" content="noindex, nofollow">
<title>{% block title %}Admin Dashboard{% endblock %} | Mon App</title>
{# 🎨 Tailwind CSS Integration (Utilisation du CDN pour la démo) #}
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
{% block stylesheets %}{% endblock %}
</head>
{# Utiliser un layout en grille ou flex pour organiser la sidebar et le contenu #}
<body class="bg-gray-100 font-sans">
<div id="admin-layout" class="flex h-screen">
{# 1. BARRE LATÉRALE (SIDEBAR - FOND BLANC) #}
<aside class="w-64 bg-white shadow-xl flex-shrink-0">
<div class="h-full flex flex-col justify-between">
{# Contenu de la navigation #}
<nav class="p-4 space-y-6 flex-1 overflow-y-auto">
{# Titre/Logo #}
<div class="text-3xl font-extrabold text-indigo-600 border-b border-gray-200 pb-4 mb-2">
E-Cosplay
</div>
{# --- Lien Principal : Dashboard --- #}
<p class="text-xs font-semibold uppercase text-gray-500 pt-4 pb-2">Général</p>
<a href="{{ path('admin_dashboard') }}"
class="block px-3 py-2 rounded-lg transition duration-150 ease-in-out text-gray-700
{% if app.request.attributes.get('_route') == 'admin_dashboard' %}
bg-indigo-100 text-indigo-700 font-bold
{% else %}
hover:bg-gray-100
{% endif %}">
Dashboard
</a>
{# --- SECTION : COMMUNAUTÉ --- #}
<p class="text-xs font-semibold uppercase text-gray-500 pt-4 pb-2">Gestion Communauté</p>
{# MEMBRES #}
<a href="{{ path('admin_members') }}"
class="block px-3 py-2 rounded-lg transition duration-150 ease-in-out text-gray-700
{% if 'members' in app.request.attributes.get('_route') %}
bg-indigo-100 text-indigo-700 font-bold
{% else %}
hover:bg-gray-100
{% endif %}">
Membres
</a>
{# ÉVÉNEMENTS #}
<a href="{{ path('admin_events') }}"
class="block px-3 py-2 rounded-lg transition duration-150 ease-in-out text-gray-700
{% if 'events' in app.request.attributes.get('_route') %}
bg-indigo-100 text-indigo-700 font-bold
{% else %}
hover:bg-gray-100
{% endif %}">
Événements
</a>
{# --- SECTION : ADMINISTRATION --- #}
<p class="text-xs font-semibold uppercase text-gray-500 pt-4 pb-2">Administration</p>
{# COMPTES ADMINISTRATEUR #}
<a href="{{ path('admin_accounts_list') }}"
class="block px-3 py-2 rounded-lg transition duration-150 ease-in-out text-gray-700
{% if 'accounts_list' in app.request.attributes.get('_route') %}
bg-indigo-100 text-indigo-700 font-bold
{% else %}
hover:bg-gray-100
{% endif %}">
Comptes Administrateur
</a>
</nav>
{# Bouton Déconnexion (pied de page de la sidebar) #}
<div class="p-4 border-t border-gray-200">
<a href="{{ path('app_logout') }}"
class="block px-3 py-2 rounded-lg text-red-500 hover:bg-red-50 transition duration-150 ease-in-out">
Déconnexion
</a>
</div>
</div>
</aside>
{# 2. CONTENU PRINCIPAL (Inclut la Topbar) #}
<div class="flex-1 flex flex-col overflow-hidden">
{# 3. TOP BAR (BARRE SUPÉRIEURE) #}
<header class="w-full bg-white shadow-md p-4 flex justify-end items-center flex-shrink-0">
{# Remplacer 'current_user.name' par la variable Twig de votre session #}
<div class="text-gray-700 font-medium">
Bienvenue, {{ app.user.username }}
</div>
{# Vous pourriez ajouter ici un bouton de profil ou un dropdown #}
</header>
{# ZONE DE CONTENU PRINCIPALE #}
<main class="flex-1 overflow-x-hidden overflow-y-auto p-6">
<h2 class="text-3xl font-semibold text-gray-800 mb-6">
{% block page_title %}{% endblock %}
</h2>
<div class="bg-white shadow-md rounded-lg min-h-full">
{% block body %}
{% endblock %}
</div>
</main>
</div>
</div>
{% block javascripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,3 @@
{% extends 'admin/base.twig' %}
{% block title %}Tableau de bord{% endblock %}
{% block page_title %}Tableau de bord{% endblock %}

View File

@@ -0,0 +1,79 @@
{% extends 'admin/base.twig' %}
{% block title %}Ajouter/Éditer un Membre{% endblock %}
{% block page_title %}
{{ form.vars.value.id ? 'Éditer le Membre' : 'Créer un nouveau Membre' }}
{% endblock %}
{% block body %}
<div class="max-w-4xl mx-auto bg-white p-8 rounded-lg">
{{ form_start(form, {'attr': {'class': 'space-y-6'}}) }}
{# --- SECTION 1: Informations de base --- #}
<h3 class="text-xl font-semibold text-gray-700 border-b pb-2 mb-4">Informations Principales</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{# Pseudo #}
{{ form_row(form.pseudo, {'attr': {'class': 'w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500'}}) }}
{# Rôle #}
{{ form_row(form.role, {'attr': {'class': 'w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500'}}) }}
</div>
{{ form_row(form.members, {
'label': 'Photo',
'attr': {'class': 'block w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 focus:outline-none p-2'}
}) }}
{# Orientation #}
<div class="mt-6">
{{ form_row(form.orientation, {'attr': {'class': 'w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500'}}) }}
</div>
{# --- SECTION 2: Tags / Booléens (Boutons Radio) --- #}
<h3 class="text-xl font-semibold text-gray-700 border-b pb-2 pt-6 mb-4">Tags et Caractéristiques</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
{# Crosscosplayer #}
{{ form_row(form.crosscosplayer, {
'label': 'Crosscosplayer ?',
'row_attr': {'class': 'space-y-4'},
'label_attr': {'class': 'block text-sm font-medium text-gray-700 mb-2'}
}) }}
{# Cosplayer #}
{{ form_row(form.cosplayer, {
'label': 'Cosplayer ?',
'row_attr': {'class': 'space-y-4'},
'label_attr': {'class': 'block text-sm font-medium text-gray-700 mb-2'}
}) }}
{# Trans #}
{{ form_row(form.trans, {
'label': 'Transgenre ?',
'row_attr': {'class': 'space-y-4'},
'label_attr': {'class': 'block text-sm font-medium text-gray-700 mb-2'}
}) }}
</div>
{# --- Bouton de Soumission --- #}
<div class="pt-6 border-t border-gray-200 mt-8">
<button type="submit" class="w-full inline-flex justify-center py-3 px-6 border border-transparent
rounded-md shadow-sm text-lg font-medium text-white bg-indigo-600
hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2
focus:ring-indigo-500 transition duration-150">
{{ form.vars.value.id ? 'Sauvegarder les Modifications' : 'Créer le Membre' }}
</button>
</div>
{{ form_end(form) }}
</div>
{# --- Rendu des erreurs et champs cachés --- #}
{{ form_end(form) }}
{% endblock %}

View File

@@ -0,0 +1,79 @@
{% extends 'admin/base.twig' %}
{% block title %}Membre(s){% endblock %}
{% block page_title %}Liste des Membres{% endblock %}
{% block body %}
<style>
.dz{
display: block;
text-align: center;
width: 100%;
}
</style>
<div class="flex justify-end">
<a href="{{ path('admin_member_create') }}"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium
shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2
focus:ring-offset-2 focus:ring-indigo-500 transition duration-150 dz">
{# Vous pouvez ajouter ici une icône si vous le souhaitez, par exemple : [Icône] #}
Créer un membre
</a>
</div>
<div class="overflow-x-auto bg-white rounded-lg">
<table class="min-w-full divide-y divide-gray-200">
{# --- EN-TÊTE DU TABLEAU (HEAD) --- #}
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Pseudo
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Rôle
</th>
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
{# --- CORPS DU TABLEAU (BODY) --- #}
<tbody class="bg-white divide-y divide-gray-200">
{# Démonstration: Boucle sur une liste de membres (members) passée par votre contrôleur #}
{% if members is not empty %}
{% for member in members %}
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">{{ member.pseudo }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
{# Utilisation d'un badge Tailwind pour le rôle #}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
{{ member.role }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href="{{ path('admin_member_edit', {id: member.id}) }}" class="text-indigo-600 hover:text-indigo-900 mr-4">
Éditer
</a>
<a href="{{ path('admin_member_delete', {id: member.id}) }}" class="text-red-600 hover:text-red-900">
Supprimer
</a>
</td>
</tr>
{% endfor %}
{% else %}
{# Message si la liste est vide #}
<tr>
<td colspan="4" class="px-6 py-12 text-center text-gray-500">
Aucun membre trouvé.
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
{% endblock %}

View File

@@ -186,6 +186,9 @@
<div class="block px-4 py-2 text-sm text-gray-900 font-semibold border-b border-gray-100">
{{ 'logged_in_as'|trans }} {{ app.user.username|default('Compte') }}
</div>
<a href="{{ path('admin_dashboard') }}" class="block px-4 py-2 text-sm text-cyan-600 hover:bg-gray-100" role="menuitem" tabindex="-1">
{{ 'logged_admin'|trans }}
</a>
<a href="{{ path('app_logout') }}" class="block px-4 py-2 text-sm text-red-600 hover:bg-gray-100" role="menuitem" tabindex="-1">
{{ 'logout_link'|trans }}
</a>

View File

@@ -38,7 +38,7 @@
{# Display error messages if login fails #}
{% if error %}
<div class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg" role="alert">
{{ dump(error) }}
{{ error.messageKey|trans(error.messageData, 'security') }}
</div>
{% endif %}
{% for message in app.flashes('success') %}

View File

@@ -567,3 +567,4 @@ open_user_menu_sr: Open user menu
logged_in_as: Signed in as
logout_link: Log Out
page.login: Login
logged_admin: Administration

View File

@@ -509,3 +509,4 @@ open_user_menu_sr: Ouvrir le menu utilisateur
logged_in_as: Connecté en tant que
logout_link: Déconnexion
page.login: Connexion
logged_admin: Administration