```
✨ feat(ReserverController): Crée la logique d'enregistrement client et envoie un email de bienvenue.
```
This commit is contained in:
@@ -1,111 +1,77 @@
|
||||
import './reserve.scss';
|
||||
// On ne fait plus d'import statique de Sentry ici
|
||||
import {UtmEvent, UtmAccount} from "./tools/UtmEvent.js";
|
||||
import { UtmEvent, UtmAccount } from "./tools/UtmEvent.js";
|
||||
import * as Turbo from "@hotwired/turbo";
|
||||
|
||||
// --- DÉTECTION BOT / PERFORMANCE ---
|
||||
const isLighthouse = () => {
|
||||
// Check if navigator and userAgent are available
|
||||
if (!navigator || !navigator.userAgent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Normalize the user agent to lowercase for robust matching
|
||||
if (typeof navigator === 'undefined' || !navigator.userAgent) return false;
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
|
||||
// Comprehensive array of Lighthouse-related user agent patterns
|
||||
const lighthousePatterns = [
|
||||
'chrome-lighthouse',
|
||||
'google/lighthouse',
|
||||
'lighthouse',
|
||||
'pagespeed',
|
||||
'pagespeedinsights',
|
||||
'googleads-lighthouse',
|
||||
'headless',
|
||||
'webdriver'
|
||||
];
|
||||
|
||||
// Use some() to check if any pattern exists in the user agent
|
||||
return lighthousePatterns.some(pattern =>
|
||||
userAgent.includes(pattern)
|
||||
);
|
||||
const patterns = ['chrome-lighthouse', 'google/lighthouse', 'lighthouse', 'pagespeed', 'headless', 'webdriver'];
|
||||
return patterns.some(pattern => userAgent.includes(pattern));
|
||||
};
|
||||
// --- INITIALISATION SENTRY (OPTIMISÉE BOT/LIGHTHOUSE) ---
|
||||
const initSentry = async () => {
|
||||
// On vérifie si on n'est pas un bot (la variable est passée par le PHP/Twig ou détectée en JS)
|
||||
|
||||
if (!isLighthouse()) {
|
||||
// --- INITIALISATION SENTRY ---
|
||||
const initSentry = async () => {
|
||||
if (!isLighthouse() && !window.SentryInitialized) {
|
||||
try {
|
||||
const Sentry = await import("@sentry/browser");
|
||||
|
||||
Sentry.init({
|
||||
dsn: "https://803814be6540031b1c37bf92ba9c0f79@sentry.esy-web.dev/24",
|
||||
tunnel: "/sentry-tunnel",
|
||||
integrations: [Sentry.browserTracingIntegration()],
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
window.SentryInitialized = true; // Empêche la ré-initialisation
|
||||
} catch (e) {
|
||||
|
||||
console.warn("Sentry load failed", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// --- LOGIQUE DU REDIRECT ---
|
||||
const initAutoRedirect = () => {
|
||||
const container = document.getElementById('payment-check-container');
|
||||
if (container && container.dataset.autoRedirect) {
|
||||
const url = container.dataset.autoRedirect;
|
||||
setTimeout(() => {
|
||||
if (document.getElementById('payment-check-container')) {
|
||||
Turbo.visit(url);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
// --- LOGIQUE DU LOADER TURBO ---
|
||||
// --- LOGIQUE DU LOADER TURBO (Unique à travers les pages) ---
|
||||
const initLoader = () => {
|
||||
let loaderEl = document.getElementById('turbo-loader');
|
||||
if (!loaderEl) {
|
||||
loaderEl = document.createElement('div');
|
||||
loaderEl.id = 'turbo-loader';
|
||||
loaderEl.className = 'fixed inset-0 z-[9999] flex items-center justify-center bg-white transition-opacity duration-300 opacity-0 pointer-events-none';
|
||||
loaderEl.innerHTML = `
|
||||
<div class="relative flex items-center justify-center">
|
||||
<div class="absolute w-24 h-24 border-4 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
|
||||
<img src="/provider/images/favicon.webp" class="w-12 h-12 relative z-10 animate-pulse" alt="Logo">
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(loaderEl);
|
||||
}
|
||||
if (document.getElementById('turbo-loader')) return;
|
||||
|
||||
const showLoader = () => {
|
||||
loaderEl.classList.remove('opacity-0', 'pointer-events-none');
|
||||
loaderEl.classList.add('opacity-100');
|
||||
};
|
||||
const loaderEl = document.createElement('div');
|
||||
loaderEl.id = 'turbo-loader';
|
||||
loaderEl.className = 'fixed inset-0 z-[9999] flex items-center justify-center bg-white transition-opacity duration-300 opacity-0 pointer-events-none';
|
||||
loaderEl.innerHTML = `
|
||||
<div class="relative flex items-center justify-center">
|
||||
<div class="absolute w-24 h-24 border-4 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
|
||||
<img src="/provider/images/favicon.webp" class="w-12 h-12 relative z-10 animate-pulse" alt="Logo">
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(loaderEl);
|
||||
|
||||
document.addEventListener("turbo:click", () => {
|
||||
loaderEl.classList.replace('opacity-0', 'opacity-100');
|
||||
loaderEl.classList.remove('pointer-events-none');
|
||||
});
|
||||
|
||||
const hideLoader = () => {
|
||||
setTimeout(() => {
|
||||
loaderEl.classList.remove('opacity-100');
|
||||
loaderEl.classList.add('opacity-0', 'pointer-events-none');
|
||||
loaderEl.classList.replace('opacity-100', 'opacity-0');
|
||||
loaderEl.classList.add('pointer-events-none');
|
||||
}, 300);
|
||||
};
|
||||
|
||||
document.addEventListener("turbo:click", showLoader);
|
||||
document.addEventListener("turbo:submit-start", showLoader);
|
||||
document.addEventListener("turbo:load", hideLoader);
|
||||
document.addEventListener("turbo:render", hideLoader);
|
||||
};
|
||||
|
||||
// --- LOGIQUE DU MENU MOBILE ---
|
||||
// --- LOGIQUE DU MENU MOBILE (Compatible Turbo) ---
|
||||
const initMobileMenu = () => {
|
||||
const btn = document.getElementById('menu-button');
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
|
||||
if (btn && menu) {
|
||||
btn.onclick = () => {
|
||||
// On enlève l'ancien listener pour éviter les doublons au retour arrière
|
||||
btn.onclick = null;
|
||||
btn.addEventListener('click', () => {
|
||||
const isExpanded = btn.getAttribute('aria-expanded') === 'true';
|
||||
btn.setAttribute('aria-expanded', !isExpanded);
|
||||
menu.classList.toggle('hidden');
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -115,56 +81,90 @@ const initCatalogueSearch = () => {
|
||||
const products = document.querySelectorAll('.product-item');
|
||||
const emptyMsg = document.getElementById('empty-msg');
|
||||
|
||||
if (!filters.length) return;
|
||||
if (filters.length === 0) return;
|
||||
|
||||
filters.forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
const category = btn.getAttribute('data-filter').toLowerCase();
|
||||
let count = 0;
|
||||
|
||||
filters.forEach(f => {
|
||||
f.classList.remove('bg-slate-900', 'text-white');
|
||||
f.classList.add('bg-white', 'text-slate-500');
|
||||
});
|
||||
btn.classList.add('bg-slate-900', 'text-white');
|
||||
btn.classList.remove('bg-white', 'text-slate-500');
|
||||
// Update UI des filtres
|
||||
filters.forEach(f => f.classList.replace('bg-slate-900', 'bg-white'));
|
||||
filters.forEach(f => f.classList.replace('text-white', 'text-slate-500'));
|
||||
btn.classList.replace('bg-white', 'bg-slate-900');
|
||||
btn.classList.replace('text-slate-500', 'text-white');
|
||||
|
||||
// Filtrage des produits
|
||||
products.forEach(item => {
|
||||
const itemCat = item.getAttribute('data-category').toLowerCase();
|
||||
if (category === 'all' || itemCat.includes(category)) {
|
||||
item.style.display = 'block';
|
||||
count++;
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
const isVisible = category === 'all' || itemCat.includes(category);
|
||||
item.style.display = isVisible ? 'block' : 'none';
|
||||
if (isVisible) count++;
|
||||
});
|
||||
|
||||
if (emptyMsg) {
|
||||
count === 0 ? emptyMsg.classList.remove('hidden') : emptyMsg.classList.add('hidden');
|
||||
}
|
||||
if (emptyMsg) count === 0 ? emptyMsg.classList.remove('hidden') : emptyMsg.classList.add('hidden');
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// --- INITIALISATION ---
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initSentry(); // Appel asynchrone de Sentry
|
||||
initLoader();
|
||||
initMobileMenu();
|
||||
initCatalogueSearch();
|
||||
initAutoRedirect();
|
||||
// --- LOGIQUE DU REDIRECT ---
|
||||
const initAutoRedirect = () => {
|
||||
const container = document.getElementById('payment-check-container');
|
||||
if (container && container.dataset.autoRedirect) {
|
||||
const url = container.dataset.autoRedirect;
|
||||
setTimeout(() => {
|
||||
// On vérifie que l'utilisateur est toujours sur la page de paiement
|
||||
if (document.getElementById('payment-check-container')) {
|
||||
Turbo.visit(url);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
const initRegisterLogic = () => {
|
||||
const siretContainer = document.getElementById('siret-container');
|
||||
const typeRadios = document.querySelectorAll('input[name="type"]');
|
||||
|
||||
if (!siretContainer || typeRadios.length === 0) return;
|
||||
|
||||
const updateSiretVisibility = () => {
|
||||
const selectedType = document.querySelector('input[name="type"]:checked')?.value;
|
||||
if (selectedType === 'buisness') {
|
||||
siretContainer.classList.remove('hidden');
|
||||
siretContainer.querySelector('input')?.setAttribute('required', 'required');
|
||||
} else {
|
||||
siretContainer.classList.add('hidden');
|
||||
siretContainer.querySelector('input')?.removeAttribute('required');
|
||||
}
|
||||
};
|
||||
|
||||
typeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', updateSiretVisibility);
|
||||
});
|
||||
|
||||
// Initialisation au chargement (si redirection avec erreur par exemple)
|
||||
updateSiretVisibility();
|
||||
};
|
||||
// --- INITIALISATION GLOBALE ---
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initSentry();
|
||||
initLoader();
|
||||
|
||||
// Custom Elements (une seule fois suffit)
|
||||
if (!customElements.get('utm-event')) customElements.define('utm-event', UtmEvent);
|
||||
if (!customElements.get('utm-account')) customElements.define('utm-account', UtmAccount);
|
||||
});
|
||||
|
||||
// À chaque changement de page Turbo
|
||||
document.addEventListener('turbo:load', () => {
|
||||
initMobileMenu();
|
||||
initCatalogueSearch();
|
||||
initAutoRedirect();
|
||||
initRegisterLogic();
|
||||
});
|
||||
|
||||
// Nettoyage avant la mise en cache de Turbo (évite les bugs visuels au retour arrière)
|
||||
document.addEventListener("turbo:before-cache", () => {
|
||||
document.querySelectorAll('.product-item').forEach(i => i.style.display = 'block');
|
||||
if (document.getElementById('empty-msg')) document.getElementById('empty-msg').classList.add('hidden');
|
||||
const emptyMsg = document.getElementById('empty-msg');
|
||||
if (emptyMsg) emptyMsg.classList.add('hidden');
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Controller;
|
||||
|
||||
use App\Entity\Account;
|
||||
use App\Entity\AccountResetPasswordRequest;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Product;
|
||||
use App\Form\RequestPasswordConfirmType;
|
||||
use App\Form\RequestPasswordRequestType;
|
||||
@@ -123,11 +124,45 @@ class ReserverController extends AbstractController
|
||||
return $this->redirectToRoute('reservation');
|
||||
}
|
||||
#[Route('/reservation/creation-compte', name: 'reservation_register')]
|
||||
public function revervationRegister(): Response
|
||||
{
|
||||
return $this->render('revervation/register.twig',[
|
||||
public function revervationRegister(
|
||||
Request $request,
|
||||
Mailer $mailer,
|
||||
EntityManagerInterface $em,
|
||||
UserPasswordHasherInterface $hasher
|
||||
): Response {
|
||||
if ($request->isMethod('POST')) {
|
||||
$payload = $request->getPayload();
|
||||
|
||||
]);
|
||||
$customer = new Customer();
|
||||
$customer->setEmail($payload->getString('email'));
|
||||
$customer->setName($payload->getString('name'));
|
||||
$customer->setSurname($payload->getString('surname'));
|
||||
$customer->setPhone($payload->getString('phone'));
|
||||
$customer->setCiv($payload->getString('civ'));
|
||||
$customer->setType($payload->getString('type')); // 'particular' ou 'buisness'
|
||||
|
||||
if ($customer->getType() === 'buisness') {
|
||||
$customer->setSiret($payload->getString('siret'));
|
||||
}
|
||||
|
||||
// Hachage du mot de passe
|
||||
$hashedPassword = $hasher->hashPassword($customer, $payload->getString('password'));
|
||||
$customer->setPassword($hashedPassword);
|
||||
$customer->setRoles(['ROLE_USER']);
|
||||
$mailer->send($customer->getEmail(),
|
||||
$customer->getName()." ".$customer->getSurname(),
|
||||
"[Ludikevent] - Code de récupération",
|
||||
"mails/welcome.twig",[
|
||||
'account' => $customer,
|
||||
]);
|
||||
$em->persist($customer);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', 'Votre compte a été créé avec succès ! Connectez-vous.');
|
||||
return $this->redirectToRoute('reservation_login');
|
||||
}
|
||||
|
||||
return $this->render('revervation/register.twig');
|
||||
}
|
||||
#[Route('/reservation/mot-de-passe', name: 'reservation_password')]
|
||||
public function forgotPassword(
|
||||
|
||||
49
templates/mails/welcome.twig
Normal file
49
templates/mails/welcome.twig
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends 'mails/base.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<mj-section padding-top="40px">
|
||||
<mj-column>
|
||||
<mj-image width="180px" src="https://votre-site.fr/images/logo.png" alt="Ludik Event" />
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<mj-section>
|
||||
<mj-column background-color="#ffffff" padding="40px" border-radius="30px">
|
||||
<mj-text align="center" css-class="hero-title">
|
||||
Bienvenue parmi nous, <span class="brand-color">{{ datas.customer.surname }}</span> !
|
||||
</mj-text>
|
||||
|
||||
<mj-image src="https://votre-site.fr/images/welcome-banner.jpg" alt="Fête Ludik Event" border-radius="20px" padding-top="20px" />
|
||||
|
||||
<mj-text align="center" padding-top="30px">
|
||||
Nous sommes ravis de vous compter parmi nos clients. Votre compte est désormais actif, vous pouvez dès à présent explorer notre catalogue et planifier votre prochain événement.
|
||||
</mj-text>
|
||||
|
||||
<mj-button href="https://votre-site.fr/reservation" padding-top="20px">
|
||||
Découvrir le catalogue
|
||||
</mj-button>
|
||||
|
||||
<mj-text align="center" font-size="14px" color="#9ca3af" padding-top="20px">
|
||||
Besoin d'aide ? Notre équipe est à votre disposition pour vous accompagner dans votre projet.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<mj-section padding-top="0px">
|
||||
<mj-column width="33.33%" padding="10px">
|
||||
<mj-text align="center" font-size="12px" font-weight="bold" color="#2563eb">
|
||||
🎈 +100 Structures
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="33.33%" padding="10px">
|
||||
<mj-text align="center" font-size="12px" font-weight="bold" color="#2563eb">
|
||||
🚚 Livraison Rapide
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="33.33%" padding="10px">
|
||||
<mj-text align="center" font-size="12px" font-weight="bold" color="#2563eb">
|
||||
✅ 100% Sécurisé
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
{% endblock %}
|
||||
91
templates/revervation/register.twig
Normal file
91
templates/revervation/register.twig
Normal file
@@ -0,0 +1,91 @@
|
||||
{% extends 'revervation/base.twig' %}
|
||||
|
||||
{% block title %}Créer mon compte | Ludik Event{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="max-w-2xl mx-auto my-12 px-4">
|
||||
<div class="bg-white rounded-[3rem] p-8 md:p-12 shadow-2xl shadow-blue-100 border border-gray-50">
|
||||
|
||||
<div class="text-center mb-10">
|
||||
<h1 class="text-4xl font-black text-gray-900 tracking-tight">Rejoignez Ludik Event</h1>
|
||||
<p class="text-gray-400 mt-2 font-medium">Créez votre compte pour gérer vos réservations</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
{# Type de compte #}
|
||||
<div class="md:col-span-2 flex p-1 bg-gray-100 rounded-2xl">
|
||||
<label class="flex-1 text-center py-3 rounded-xl cursor-pointer transition-all font-bold text-sm has-[:checked]:bg-white has-[:checked]:text-blue-600 has-[:checked]:shadow-sm text-gray-500">
|
||||
<input type="radio" name="type" value="particular" class="hidden" checked onchange="toggleSiret(false)">
|
||||
Particulier
|
||||
</label>
|
||||
<label class="flex-1 text-center py-3 rounded-xl cursor-pointer transition-all font-bold text-sm has-[:checked]:bg-white has-[:checked]:text-blue-600 has-[:checked]:shadow-sm text-gray-500">
|
||||
<input type="radio" name="type" value="buisness" class="hidden" onchange="toggleSiret(true)">
|
||||
Professionnel / Asso
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{# Civilité #}
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-xs font-bold text-gray-400 uppercase mb-2 ml-2">Civilité</label>
|
||||
<select name="civ" class="w-full bg-gray-50 border-none rounded-2xl px-6 py-4 focus:ring-2 focus:ring-blue-500 outline-none appearance-none font-medium">
|
||||
<option value="M.">Monsieur (M.)</option>
|
||||
<option value="Mme">Madame (Mme)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{# Nom / Prénom #}
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-400 uppercase mb-2 ml-2">Nom</label>
|
||||
<input type="text" name="name" required placeholder="Ex: Dupont"
|
||||
class="w-full bg-gray-50 border-none rounded-2xl px-6 py-4 focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-400 uppercase mb-2 ml-2">Prénom</label>
|
||||
<input type="text" name="surname" required placeholder="Ex: Jean"
|
||||
class="w-full bg-gray-50 border-none rounded-2xl px-6 py-4 focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
|
||||
{# Email / Tel #}
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-400 uppercase mb-2 ml-2">Email</label>
|
||||
<input type="email" name="email" required placeholder="jean@mail.com"
|
||||
class="w-full bg-gray-50 border-none rounded-2xl px-6 py-4 focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-gray-400 uppercase mb-2 ml-2">Téléphone</label>
|
||||
<input type="tel" name="phone" required placeholder="06 00 00 00 00"
|
||||
class="w-full bg-gray-50 border-none rounded-2xl px-6 py-4 focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
|
||||
{# SIRET (Caché par défaut) #}
|
||||
<div id="siret-container" class="md:col-span-2 hidden">
|
||||
<label class="block text-xs font-bold text-gray-400 uppercase mb-2 ml-2">Numéro SIRET</label>
|
||||
<input type="text" name="siret" placeholder="123 456 789 00012"
|
||||
class="w-full bg-gray-50 border-none rounded-2xl px-6 py-4 focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
|
||||
{# Mot de passe #}
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-xs font-bold text-gray-400 uppercase mb-2 ml-2">Mot de passe</label>
|
||||
<input type="password" name="password" required placeholder="••••••••"
|
||||
class="w-full bg-gray-50 border-none rounded-2xl px-6 py-4 focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2 pt-4">
|
||||
<button type="submit" class="w-full bg-blue-600 text-white font-black py-5 rounded-2xl shadow-xl shadow-blue-100 hover:bg-blue-700 transition-all transform active:scale-95 uppercase tracking-widest text-sm">
|
||||
Créer mon compte gratuit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-10 text-center border-t border-gray-50 pt-8">
|
||||
<p class="text-gray-400 text-sm font-medium">
|
||||
Déjà client ?
|
||||
<a href="{{ path('reservation_login') }}" class="text-blue-600 font-bold hover:underline ml-1">Connectez-vous ici</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user