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:
Serreau Jovann
2025-11-17 13:12:56 +01:00
parent 24406d0184
commit 5930f0435f
9 changed files with 507 additions and 32 deletions

View File

@@ -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;
}
}
});

View 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
{
}
}

View File

@@ -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,

View File

@@ -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>

View 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 %}

View 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 %}

View 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 %}

View File

@@ -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.

View File

@@ -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)