✨ feat(assets/app.js): Ajoute la gestion des menus utilisateur et améliore l'UI.
Ajoute la gestion des menus utilisateur (desktop et mobile) avec des fonctions pour basculer la visibilité et ferme les menus au clic extérieur. Ajoute aussi la gestion de la touche "Echap" pour fermer les menus. ✨ feat(translations/messages.en.yaml): Add security translations for login & password. Ajoute les traductions anglaises pour la sécurité (connexion, mot de passe oublié). ✨ feat(translations/messages.fr.yaml): Ajoute les traductions pour la sécurité. Ajoute les traductions françaises pour les formulaires de connexion et mot de passe oublié. ✨ feat(templates/security): Crée les templates pour login et mot de passe oublié. Crée les templates login.twig, forgot_password.twig et forgot_password_success.twig. ✨ feat(src/Service/ResetPassword): Adapte ResetPasswordSubscriber pour E-Cosplay. Adapte le service ResetPasswordSubscriber pour le projet E-Cosplay. ✨ feat(src/Controller/SecurityController): Crée le contrôleur de sécurité. Crée le SecurityController avec les routes pour la connexion et la gestion du mot de passe oublié. ✨ feat(templates/base.twig): Ajoute le menu utilisateur desktop et mobile. Ajoute le menu utilisateur (desktop et mobile) avec gestion de la connexion/déconnexion.
This commit is contained in:
@@ -1,22 +1,43 @@
|
||||
import './app.scss'
|
||||
import * as Turbo from "@hotwired/turbo"
|
||||
|
||||
/**
|
||||
* Fonction générique pour basculer la visibilité d'un menu déroulant.
|
||||
* @param {HTMLElement} button - Le bouton qui déclenche l'action.
|
||||
* @param {HTMLElement} menu - Le menu à afficher/masquer.
|
||||
*/
|
||||
function toggleMenu(button, menu) {
|
||||
if (!button || !menu) return;
|
||||
const isExpanded = button.getAttribute('aria-expanded') === 'true' || false;
|
||||
button.setAttribute('aria-expanded', !isExpanded);
|
||||
menu.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonction d'initialisation pour les composants qui DOIVENT être réinitialisés
|
||||
* après un chargement Turbo (comme les compteurs d'articles, les états initiaux).
|
||||
* Le menu mobile et le panier sont gérés par délégation d'événements.
|
||||
*/
|
||||
function initializeUI() {
|
||||
// Réinitialisation des états des menus cachés après un chargement Turbo,
|
||||
// au cas où ils étaient ouverts lors de la navigation précédente.
|
||||
document.querySelectorAll('#mobile-menu, #userMenuDesktop, #userMenuMobile').forEach(menu => {
|
||||
if (!menu.classList.contains('hidden')) {
|
||||
menu.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('#mobileMenuButton, #userMenuButtonDesktop, #userMenuButtonMobile').forEach(button => {
|
||||
button.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
|
||||
|
||||
// --- 2. Gestion du Panier Latéral (Off-Canvas) ---
|
||||
// Les fonctions open/close ont besoin de l'accès direct aux éléments,
|
||||
// mais les listeners d'ouverture/fermeture seront gérés par délégation en bas.
|
||||
const cartSidebar = document.getElementById('cartSidebar');
|
||||
const cartBackdrop = document.getElementById('cartBackdrop');
|
||||
const closeCartButton = document.getElementById('closeCartButton');
|
||||
|
||||
// Mettez les fonctions ici pour qu'elles soient toujours définies si les éléments existent
|
||||
if (cartSidebar && cartBackdrop && closeCartButton) {
|
||||
// ... (Fonctions openCart et closeCart inchangées)
|
||||
function openCart() {
|
||||
document.body.style.overflow = 'hidden';
|
||||
cartBackdrop.classList.remove('hidden');
|
||||
@@ -36,12 +57,15 @@ function initializeUI() {
|
||||
// Stocker les fonctions dans une variable globale accessible par l'écouteur du document
|
||||
window.openCart = openCart;
|
||||
window.closeCart = closeCart;
|
||||
} else {
|
||||
// Sécurité si les éléments du panier n'existent pas
|
||||
window.openCart = null;
|
||||
window.closeCart = null;
|
||||
}
|
||||
|
||||
|
||||
// --- 3. Logique Panier Mock (Affichage du compteur) ---
|
||||
function updateCartDisplay(count) {
|
||||
// ... (Logique inchangée)
|
||||
const desktopCounter = document.getElementById('cartCountDesktop');
|
||||
const mobileCounter = document.getElementById('cartCountMobile');
|
||||
|
||||
@@ -73,20 +97,55 @@ document.addEventListener('turbo:load', initializeUI);
|
||||
document.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
|
||||
// 1. GESTION DU MENU MOBILE (Burger)
|
||||
// --- 1. GESTION DU MENU MOBILE (Burger) ---
|
||||
const mobileMenuButton = document.getElementById('mobileMenuButton');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
|
||||
// On vérifie si la cible cliquée est le bouton ou un de ses enfants
|
||||
if (mobileMenuButton && mobileMenu && (target === mobileMenuButton || mobileMenuButton.contains(target))) {
|
||||
event.preventDefault(); // Empêche l'action par défaut du bouton
|
||||
const isExpanded = mobileMenuButton.getAttribute('aria-expanded') === 'true';
|
||||
mobileMenuButton.setAttribute('aria-expanded', !isExpanded);
|
||||
mobileMenu.classList.toggle('hidden');
|
||||
event.preventDefault();
|
||||
toggleMenu(mobileMenuButton, mobileMenu);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. GESTION DE L'OUVERTURE ET FERMETURE DU PANIER
|
||||
// --- 2. GESTION DU MENU UTILISATEUR (Dropdown) ---
|
||||
const userMenuButtonDesktop = document.getElementById('userMenuButtonDesktop');
|
||||
const userMenuDesktop = document.getElementById('userMenuDesktop');
|
||||
const userMenuButtonMobile = document.getElementById('userMenuButtonMobile');
|
||||
const userMenuMobile = document.getElementById('userMenuMobile');
|
||||
|
||||
// Ouverture/Fermeture du menu utilisateur Desktop
|
||||
if (userMenuButtonDesktop && userMenuDesktop && (target === userMenuButtonDesktop || userMenuButtonDesktop.contains(target))) {
|
||||
event.preventDefault();
|
||||
// S'assurer que les autres menus sont fermés
|
||||
userMenuMobile.classList.add('hidden');
|
||||
toggleMenu(userMenuButtonDesktop, userMenuDesktop);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ouverture/Fermeture du menu utilisateur Mobile
|
||||
if (userMenuButtonMobile && userMenuMobile && (target === userMenuButtonMobile || userMenuButtonMobile.contains(target))) {
|
||||
event.preventDefault();
|
||||
// S'assurer que les autres menus sont fermés
|
||||
userMenuDesktop.classList.add('hidden');
|
||||
toggleMenu(userMenuButtonMobile, userMenuMobile);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fermeture des menus s'il y a un clic en dehors
|
||||
const isClickInsideDesktopMenu = userMenuDesktop && (userMenuDesktop.contains(target) || userMenuButtonDesktop.contains(target));
|
||||
const isClickInsideMobileMenu = userMenuMobile && (userMenuMobile.contains(target) || userMenuButtonMobile.contains(target));
|
||||
|
||||
if (userMenuDesktop && userMenuButtonDesktop && !isClickInsideDesktopMenu) {
|
||||
userMenuDesktop.classList.add('hidden');
|
||||
userMenuButtonDesktop.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
if (userMenuMobile && userMenuButtonMobile && !isClickInsideMobileMenu) {
|
||||
userMenuMobile.classList.add('hidden');
|
||||
userMenuButtonMobile.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
// --- 3. GESTION DE L'OUVERTURE ET FERMETURE DU PANIER ---
|
||||
const openCartDesktop = document.getElementById('openCartDesktop');
|
||||
const openCartMobile = document.getElementById('openCartMobile');
|
||||
const closeCartButton = document.getElementById('closeCartButton');
|
||||
@@ -114,12 +173,32 @@ document.addEventListener('click', (event) => {
|
||||
window.closeCart();
|
||||
return;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// --- GESTION GLOBALE DE LA TOUCHE ESC (Une seule fois) ---
|
||||
document.addEventListener('keydown', (event) => {
|
||||
const cartSidebar = document.getElementById('cartSidebar');
|
||||
// Fermer le panier
|
||||
if (cartSidebar && window.closeCart && event.key === 'Escape' && !cartSidebar.classList.contains('translate-x-full')) {
|
||||
window.closeCart();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fermer les menus utilisateur
|
||||
const userMenuDesktop = document.getElementById('userMenuDesktop');
|
||||
const userMenuButtonDesktop = document.getElementById('userMenuButtonDesktop');
|
||||
const userMenuMobile = document.getElementById('userMenuMobile');
|
||||
const userMenuButtonMobile = document.getElementById('userMenuButtonMobile');
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
if (userMenuDesktop && !userMenuDesktop.classList.contains('hidden')) {
|
||||
toggleMenu(userMenuButtonDesktop, userMenuDesktop);
|
||||
return;
|
||||
}
|
||||
if (userMenuMobile && !userMenuMobile.classList.contains('hidden')) {
|
||||
toggleMenu(userMenuButtonMobile, userMenuMobile);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
54
src/Controller/SecurityController.php
Normal file
54
src/Controller/SecurityController.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Account;
|
||||
use App\Entity\AccountResetPasswordRequest;
|
||||
use App\Form\RequestPasswordConfirmType;
|
||||
use App\Form\RequestPasswordRequestType;
|
||||
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\EventDispatcher\EventSubscriberInterface;
|
||||
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 SecurityController extends AbstractController
|
||||
{
|
||||
|
||||
#[Route(path: '/connexion', name: 'app_login', options: ['sitemap' => false], methods: ['GET','POST'])]
|
||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||
{
|
||||
return $this->render('security/login.twig', [
|
||||
'last_username' => $authenticationUtils->getLastUsername(),
|
||||
'error' => $authenticationUtils->getLastAuthenticationError(),
|
||||
]);
|
||||
}
|
||||
#[Route(path: '/mot-de-passe-oublie', name: 'app_forgot_password', options: ['sitemap' => false], methods: ['GET','POST'])]
|
||||
public function forgotPassword(Request $request,EventDispatcherInterface $eventDispatcher): Response
|
||||
{
|
||||
$requestPasswordRequest = new ResetPasswordEvent();
|
||||
$form = $this->createForm(RequestPasswordRequestType::class,$requestPasswordRequest);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$eventDispatcher->dispatch($requestPasswordRequest);
|
||||
return $this->render('security/forgot_password_success.twig', [
|
||||
]);
|
||||
}
|
||||
return $this->render('security/forgot_password.twig', [
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
#[Route(path: '/mot-de-passe-oublie/{id}/{token}', name: 'app_forgot_password_confirm', options: ['sitemap' => false], methods: ['GET','POST'])]
|
||||
public function forgotPasswordConfirm(Request $request): Response
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -63,20 +63,17 @@ class ResetPasswordSubscriber
|
||||
}
|
||||
|
||||
$resetLink = $this->urlGenerator->generate(
|
||||
'app_forgotpassword_confirm',
|
||||
'app_forgot_password_confirm',
|
||||
['id' => $account->getId(), 'token' => $request->getToken()],
|
||||
UrlGeneratorInterface::ABSOLUTE_URL
|
||||
);
|
||||
|
||||
$title = "[Mainframe]";
|
||||
if($this->requestStack->getMainRequest()->getHost() == "espace-client.siteconseil.fr") {
|
||||
$title = "[SARL SITECONSEIL]";
|
||||
}
|
||||
$title = "[E-Cosplay]";
|
||||
$this->mailer->send(
|
||||
$account->getEmail(),
|
||||
$account->getUsername(),
|
||||
' - Lien pour réinitialiser votre mot de passe',
|
||||
'mails/artemis/reset.twig',
|
||||
'mails/reset.twig',
|
||||
[
|
||||
'account' => $account,
|
||||
'request' => $request,
|
||||
|
||||
@@ -167,10 +167,69 @@
|
||||
0
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{# NOUVEAU: COMPTE / MENU DÉROULANT (Desktop) #}
|
||||
<div class="relative">
|
||||
<button id="userMenuButtonDesktop" type="button" class="p-2 text-gray-700 hover:text-red-600 rounded-full transition duration-150 ease-in-out" aria-expanded="false" aria-haspopup="true">
|
||||
<span class="sr-only">{{ 'open_user_menu_sr'|trans }}</span>
|
||||
{# Icône de l'utilisateur (Heroicons 'User') #}
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{# Menu déroulant (Masqué par défaut avec 'hidden') #}
|
||||
<div id="userMenuDesktop" class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 hidden" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button">
|
||||
<div class="py-1" role="none">
|
||||
{% if is_granted('ROLE_USER') %}
|
||||
{# Afficher le nom de l'utilisateur et la déconnexion si connecté #}
|
||||
<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('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>
|
||||
{% else %}
|
||||
{# Afficher la connexion si non connecté #}
|
||||
<a href="{{ path('app_login') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem" tabindex="-1" id="user-menu-item-0">
|
||||
{{ 'login_link'|trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# BOUTONS MOBILE (Burger Icon, Panier, et Langue) #}
|
||||
{# BOUTONS MOBILE (Burger Icon, Panier, Langue, et Compte) #}
|
||||
<div class="md:hidden flex items-center space-x-2">
|
||||
{# NOUVEAU: COMPTE / MENU DÉROULANT (Mobile) #}
|
||||
<div class="relative">
|
||||
<button id="userMenuButtonMobile" type="button" class="p-2 text-gray-700 hover:text-red-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 rounded-md" aria-expanded="false" aria-haspopup="true">
|
||||
<span class="sr-only">{{ 'open_user_menu_sr'|trans }}</span>
|
||||
{# Icône de l'utilisateur (Heroicons 'User') #}
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
{# Le menu déroulant Mobile peut être affiché via le JS global si l'espace le permet #}
|
||||
<div id="userMenuMobile" class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 hidden" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button-mobile">
|
||||
<div class="py-1" role="none">
|
||||
{% if is_granted('ROLE_USER') %}
|
||||
<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('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>
|
||||
{% else %}
|
||||
<a href="{{ path('app_login') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem" tabindex="-1">
|
||||
{{ 'login_link'|trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# SÉLECTEUR DE LANGUE (Mobile - Compact) #}
|
||||
<div class="flex items-center space-x-2">
|
||||
{% set current_route = app.request.attributes.get('_route') %}
|
||||
@@ -259,18 +318,6 @@
|
||||
</svg>
|
||||
<p class="mt-1">{{ 'cart_empty'|trans }}</p>
|
||||
</div>
|
||||
|
||||
{# Exemple d'article (Commenter ou supprimer en production) #}
|
||||
{#
|
||||
<div class="flex items-center space-x-4 border-b pb-4">
|
||||
<img class="h-16 w-16 object-cover rounded" src="placeholder-image-url.jpg" alt="Produit">
|
||||
<div class="flex-grow">
|
||||
<p class="font-semibold text-gray-800">T-Shirt E-Cosplay</p>
|
||||
<p class="text-sm text-gray-600">1 x 19.99 €</p>
|
||||
</div>
|
||||
<button class="text-red-500 hover:text-red-700 text-sm">Supprimer</button>
|
||||
</div>
|
||||
#}
|
||||
</div>
|
||||
|
||||
{# Pied de page du panier (Total et Paiement) #}
|
||||
@@ -287,7 +334,7 @@
|
||||
</div>
|
||||
|
||||
{# FONDU NOIR (Backdrop) - S'affiche lorsque le panier est ouvert #}
|
||||
<div id="cartBackdrop" class="fixed inset-0 bg-op z-40 hidden transition-opacity duration-300 ease-in-out" aria-hidden="true"></div>
|
||||
<div id="cartBackdrop" class="fixed inset-0 bg-op z-40 hidden transition-opacity duration-300 ease-in-out" aria-hidden="true"></div>
|
||||
{# ========================================================== #}
|
||||
|
||||
|
||||
@@ -369,8 +416,7 @@
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{% block javascripts %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
95
templates/security/forgot_password.twig
Normal file
95
templates/security/forgot_password.twig
Normal file
@@ -0,0 +1,95 @@
|
||||
{% extends 'base.twig' %}
|
||||
|
||||
{% block title %}{{ 'events.forgot_password'|trans }}{% endblock %}
|
||||
{% block meta_description %}{{ 'events.forgot_password'|trans }}{% endblock %}
|
||||
|
||||
{% block canonical_url %}<link rel="canonical" href="{{ url('app_forgot_password') }}" />{% endblock %}
|
||||
{% block breadcrumb_schema %}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "{{ 'breadcrumb.home'|trans }}",
|
||||
"item": "{{ app.request.schemeAndHttpHost }}"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "{{ 'breadcrumb.forgot_password'|trans }}",
|
||||
"item": "{{ app.request.schemeAndHttpHost }}{{ app.request.pathInfo }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8 p-10 bg-white rounded-xl shadow-lg">
|
||||
|
||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
{{ 'events.forgot_password'|trans }}
|
||||
</h2>
|
||||
|
||||
<p class="mt-2 text-center text-sm text-gray-600">
|
||||
{{ 'text.enter_email_for_reset'|trans }}
|
||||
</p>
|
||||
|
||||
{# Affichage des messages flash (succès ou erreur) #}
|
||||
{% for flash_error in app.flashes('reset_password_error') %}
|
||||
<div class="p-4 text-sm text-red-700 bg-red-100 rounded-lg" role="alert">
|
||||
{{ flash_error }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for message in app.flashes('success') %}
|
||||
<div class="p-4 text-sm text-green-700 bg-green-100 rounded-lg" role="alert">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
{# Le formulaire Symfony #}
|
||||
{{ form_start(form, {'attr': {'class': 'mt-8 space-y-6'}}) }}
|
||||
|
||||
<div class="rounded-md shadow-sm -space-y-px">
|
||||
{# Champ Email #}
|
||||
<div>
|
||||
{{ form_label(form.email, 'label.email'|trans, {'label_attr': {'class': 'sr-only'}}) }}
|
||||
{{ form_widget(form.email, {
|
||||
'attr': {
|
||||
'class': 'appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm',
|
||||
'placeholder': 'label.email'|trans,
|
||||
'autocomplete': 'email',
|
||||
'required': 'required'
|
||||
}
|
||||
}) }}
|
||||
|
||||
{# Affichage des erreurs de champ spécifiques #}
|
||||
{{ form_errors(form.email) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Bouton Soumettre #}
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
{{ 'button.send_reset_link'|trans }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
<div class="text-center text-sm">
|
||||
<a href="{{ path('app_login') }}" class="font-medium text-indigo-600 hover:text-indigo-500">
|
||||
{{ 'link.back_to_login'|trans }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
41
templates/security/forgot_password_success.twig
Normal file
41
templates/security/forgot_password_success.twig
Normal file
@@ -0,0 +1,41 @@
|
||||
{% extends 'base.twig' %}
|
||||
|
||||
{% block title %}{{ 'events.reset_email_sent'|trans }}{% endblock %}
|
||||
{% block meta_description %}{{ 'events.reset_email_sent'|trans }}{% endblock %}
|
||||
|
||||
{% block canonical_url %}<link rel="canonical" href="{{ url('app_check_email') }}" />{% endblock %}
|
||||
{# Pas de Breadcrumb Schema pour cette page de confirmation #}
|
||||
|
||||
{% block body %}
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8 p-10 bg-white rounded-xl shadow-lg text-center">
|
||||
|
||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
{{ 'events.reset_email_sent'|trans }}
|
||||
</h2>
|
||||
|
||||
{# Message de Sécurité IMPORTANT #}
|
||||
<div class="p-6 bg-blue-50 border border-blue-200 text-blue-700 rounded-lg">
|
||||
<p class="font-medium mb-2">
|
||||
{{ 'text.check_inbox_title'|trans }}
|
||||
</p>
|
||||
<p class="text-sm">
|
||||
{{ 'text.check_inbox_description'|trans }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-sm text-gray-500">
|
||||
{{ 'text.spam_folder_tip'|trans }}
|
||||
</p>
|
||||
|
||||
{# Lien de Retour à la Connexion #}
|
||||
<div class="mt-8">
|
||||
<a href="{{ path('app_login') }}"
|
||||
class="font-medium text-indigo-600 hover:text-indigo-500">
|
||||
{{ 'link.back_to_login'|trans }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
98
templates/security/login.twig
Normal file
98
templates/security/login.twig
Normal file
@@ -0,0 +1,98 @@
|
||||
{% extends 'base.twig' %}
|
||||
|
||||
{% block title %}{{ 'events.login'|trans }}{% endblock %}
|
||||
{% block meta_description %}{{ 'events.login'|trans }}{% endblock %}
|
||||
|
||||
{% block canonical_url %}<link rel="canonical" href="{{ url('app_login') }}" />{% endblock %}
|
||||
{% block breadcrumb_schema %}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "{{ 'breadcrumb.home'|trans }}",
|
||||
"item": "{{ app.request.schemeAndHttpHost }}"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "{{ 'breadcrumb.login'|trans }}",
|
||||
"item": "{{ app.request.schemeAndHttpHost }}{{ app.request.pathInfo }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8 p-10 bg-white rounded-xl shadow-lg">
|
||||
|
||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
{{ 'security.login'|trans }}
|
||||
</h2>
|
||||
|
||||
{# 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">
|
||||
<span class="font-medium">{{ 'error.login_failed'|trans }}</span> {{ error.messageKey|trans(error.arguments, 'security') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# The actual login form #}
|
||||
<form class="mt-8 space-y-6" action="{{ path('app_login') }}" method="post">
|
||||
<input type="hidden" name="remember" value="true">
|
||||
|
||||
{# Username Field (Email) #}
|
||||
<div class="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label for="username" class="sr-only">{{ 'label.email'|trans }}</label>
|
||||
<input id="username" name="_username" type="email" autocomplete="email" required
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="{{ 'label.email'|trans }}" value="{{ last_username }}" autofocus>
|
||||
</div>
|
||||
|
||||
{# Password Field #}
|
||||
<div>
|
||||
<label for="password" class="sr-only">{{ 'label.password'|trans }}</label>
|
||||
<input id="password" name="_password" type="password" autocomplete="current-password" required
|
||||
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="{{ 'label.password'|trans }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Remember Me & Forgot Password (Optional) #}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<input id="remember_me" name="_remember_me" type="checkbox"
|
||||
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
|
||||
<label for="remember_me" class="ml-2 block text-sm text-gray-900">
|
||||
{{ 'label.remember_me'|trans }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="text-sm">
|
||||
<a href="{{ path('app_forgot_password') }}" class="font-medium text-indigo-600 hover:text-indigo-500">
|
||||
{{ 'link.forgot_password'|trans }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# CSRF Token (Important for security) #}
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||
|
||||
{# Submit Button #}
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
{{ 'button.sign_in'|trans }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -536,3 +536,34 @@ events.list_main_title: Events
|
||||
events.no_events_title: No Planned Events
|
||||
events.no_events_message: It seems there are no planned events for the moment. Check back soon!
|
||||
events.button_contact: Contact Us
|
||||
login_link: Log In
|
||||
register_link: Register
|
||||
|
||||
# translations/messages.en.yaml
|
||||
|
||||
# Login Page (Connexion)
|
||||
events.login: Login
|
||||
events.forgot_password: Forgot Password
|
||||
|
||||
# Breadcrumbs
|
||||
breadcrumb.login: Login
|
||||
breadcrumb.forgot_password: Forgot Password
|
||||
|
||||
# Labels and Buttons
|
||||
label.email: Email address
|
||||
label.password: Password
|
||||
label.remember_me: Remember me
|
||||
button.sign_in: Sign In
|
||||
button.send_reset_link: Send Reset Link
|
||||
|
||||
# Links
|
||||
link.forgot_password: Forgot your password?
|
||||
link.back_to_login: Back to login
|
||||
|
||||
# Errors and Security Messages
|
||||
error.login_failed: Login failed.
|
||||
|
||||
security.login: Sign in to your account
|
||||
|
||||
# Descriptive text
|
||||
text.enter_email_for_reset: Please enter your email address to receive a reset link.
|
||||
|
||||
@@ -524,3 +524,37 @@ events.list_main_title: Événements
|
||||
events.no_events_title: "Aucun événement planifié"
|
||||
events.no_events_message: "Il semble qu'il n'y ait aucun événement de prévu pour le moment. Revenez bientôt !"
|
||||
events.button_contact: "Nous Contacter"
|
||||
login_link: Connexion
|
||||
register_link: Inscription
|
||||
|
||||
breadcrumb.login: Connexion
|
||||
label.email: Adresse e-mail
|
||||
label.password: Mot de passe
|
||||
label.remember_me: Se souvenir de moi
|
||||
button.sign_in: Se connecter
|
||||
link.forgot_password: Mot de passe oublié ?
|
||||
error.login_failed: Échec de la connexion.
|
||||
security.login: Connexion à votre compte
|
||||
|
||||
events.forgot_password: Mot de passe oublié
|
||||
|
||||
# Breadcrumbs (Fil d'Ariane)
|
||||
breadcrumb.forgot_password: Mot de passe oublié
|
||||
|
||||
# Texte descriptif
|
||||
text.enter_email_for_reset: Veuillez entrer votre adresse e-mail pour recevoir un lien de réinitialisation.
|
||||
|
||||
# Bouton
|
||||
button.send_reset_link: Envoyer le lien de réinitialisation
|
||||
|
||||
# Liens
|
||||
link.back_to_login: Retour à la connexion
|
||||
|
||||
|
||||
events.reset_email_sent: E-mail de réinitialisation envoyé
|
||||
|
||||
text.check_inbox_title: Vérifiez votre boîte de réception 📥
|
||||
text.check_inbox_description: Un e-mail a été envoyé avec un lien pour réinitialiser votre mot de passe. Il se peut qu'il arrive dans quelques minutes.
|
||||
text.spam_folder_tip: Si vous ne le voyez pas, vérifiez votre dossier de courriers indésirables (spam).
|
||||
|
||||
# ... (Assurez-vous que 'link.back_to_login' est déjà défini)
|
||||
|
||||
Reference in New Issue
Block a user