```
✨ feat(UtmEvent): Ajoute le tracking Umami des utilisateurs connectés.
Ajoute l'identification des utilisateurs Umami et enregistre la session.
Implémente une bannière de consentement pour les cookies et gère l'état.
```
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import './reserve.scss';
|
||||
import { UtmEvent, UtmAccount } from "./tools/UtmEvent.js";
|
||||
import { CookieBanner } from "./tools/CookieBanner.js";
|
||||
import * as Turbo from "@hotwired/turbo";
|
||||
|
||||
// --- DÉTECTION BOT / PERFORMANCE ---
|
||||
@@ -10,38 +11,54 @@ const isLighthouse = () => {
|
||||
return patterns.some(pattern => userAgent.includes(pattern));
|
||||
};
|
||||
|
||||
// --- 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);
|
||||
// --- GESTION DYNAMIQUE DE SENTRY ---
|
||||
const toggleSentry = async (status) => {
|
||||
if (isLighthouse()) return;
|
||||
|
||||
try {
|
||||
const Sentry = await import("@sentry/browser");
|
||||
|
||||
if (status === 'accepted') {
|
||||
if (!window.SentryInitialized) {
|
||||
window.sentryClient = Sentry.init({
|
||||
dsn: "https://803814be6540031b1c37bf92ba9c0f79@sentry.esy-web.dev/24",
|
||||
tunnel: "/sentry-tunnel",
|
||||
integrations: [Sentry.browserTracingIntegration()],
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
window.SentryInitialized = true;
|
||||
console.log("✔️ Sentry initialisé et activé");
|
||||
} else {
|
||||
// Réactivation si déjà chargé
|
||||
if (window.sentryClient) window.sentryClient.getOptions().enabled = true;
|
||||
console.log("✔️ Sentry ré-activé");
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'refused' && window.SentryInitialized) {
|
||||
// Désactivation sans décharger le script
|
||||
if (window.sentryClient) window.sentryClient.getOptions().enabled = false;
|
||||
console.log("🛑 Sentry désactivé (Client muet)");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Sentry toggle failed", e);
|
||||
}
|
||||
};
|
||||
|
||||
// --- LOGIQUE DU LOADER TURBO (Unique à travers les pages) ---
|
||||
// --- LOGIQUE DU LOADER TURBO ---
|
||||
const initLoader = () => {
|
||||
if (document.getElementById('turbo-loader')) return;
|
||||
|
||||
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-100 pointer-events-none';
|
||||
loaderEl.innerHTML = `
|
||||
<div class="relative flex items-center justify-center">
|
||||
<div class="absolute w-24 h-24 border-4 border-[#f39e36] 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);
|
||||
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-[#f39e36] 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');
|
||||
@@ -59,13 +76,11 @@ const initLoader = () => {
|
||||
document.addEventListener("turbo:render", hideLoader);
|
||||
};
|
||||
|
||||
// --- LOGIQUE DU MENU MOBILE (Compatible Turbo) ---
|
||||
// --- LOGIQUE INTERFACE (Menu, Filtres, Redirect, Register) ---
|
||||
const initMobileMenu = () => {
|
||||
const btn = document.getElementById('menu-button');
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
|
||||
if (btn && menu) {
|
||||
// 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';
|
||||
@@ -75,57 +90,46 @@ const initMobileMenu = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// --- LOGIQUE FILTRE CATALOGUE ---
|
||||
const initCatalogueSearch = () => {
|
||||
const filters = document.querySelectorAll('.filter-btn');
|
||||
const products = document.querySelectorAll('.product-item');
|
||||
const emptyMsg = document.getElementById('empty-msg');
|
||||
|
||||
if (filters.length === 0) return;
|
||||
|
||||
filters.forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
const category = btn.getAttribute('data-filter').toLowerCase();
|
||||
let count = 0;
|
||||
|
||||
// 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'));
|
||||
filters.forEach(f => {
|
||||
f.classList.replace('bg-slate-900', 'bg-white');
|
||||
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();
|
||||
const itemCat = (item.getAttribute('data-category') || '').toLowerCase();
|
||||
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');
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// --- 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);
|
||||
}
|
||||
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') {
|
||||
@@ -136,25 +140,28 @@ const initRegisterLogic = () => {
|
||||
siretContainer.querySelector('input')?.removeAttribute('required');
|
||||
}
|
||||
};
|
||||
|
||||
typeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', updateSiretVisibility);
|
||||
});
|
||||
|
||||
// Initialisation au chargement (si redirection avec erreur par exemple)
|
||||
typeRadios.forEach(radio => radio.addEventListener('change', updateSiretVisibility));
|
||||
updateSiretVisibility();
|
||||
};
|
||||
|
||||
// --- INITIALISATION GLOBALE ---
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initSentry();
|
||||
initLoader();
|
||||
|
||||
// Custom Elements (une seule fois suffit)
|
||||
// Enregistrement Custom Elements
|
||||
if (!customElements.get('utm-event')) customElements.define('utm-event', UtmEvent);
|
||||
if (!customElements.get('utm-account')) customElements.define('utm-account', UtmAccount);
|
||||
if (!customElements.get('cookie-banner')) customElements.define('cookie-banner', CookieBanner);
|
||||
|
||||
// Initialisation Sentry basée sur le choix existant
|
||||
const currentConsent = sessionStorage.getItem('ldk_cookie');
|
||||
if (currentConsent) toggleSentry(currentConsent);
|
||||
|
||||
// Écouteurs pour changements de choix cookies
|
||||
window.addEventListener('cookieAccepted', () => toggleSentry('accepted'));
|
||||
window.addEventListener('cookieRefused', () => toggleSentry('refused'));
|
||||
});
|
||||
|
||||
// À chaque changement de page Turbo
|
||||
document.addEventListener('turbo:load', () => {
|
||||
initMobileMenu();
|
||||
initCatalogueSearch();
|
||||
@@ -162,7 +169,6 @@ document.addEventListener('turbo:load', () => {
|
||||
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');
|
||||
const emptyMsg = document.getElementById('empty-msg');
|
||||
|
||||
118
assets/tools/CookieBanner.js
Normal file
118
assets/tools/CookieBanner.js
Normal file
@@ -0,0 +1,118 @@
|
||||
export class CookieBanner extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const currentStatus = sessionStorage.getItem('ldk_cookie');
|
||||
|
||||
if (currentStatus === 'accepted' || currentStatus === 'refused') {
|
||||
this.triggerEvent(currentStatus);
|
||||
this.renderTrigger(currentStatus); // On passe le statut au trigger
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentStatus) {
|
||||
sessionStorage.setItem('ldk_cookie', 'non');
|
||||
}
|
||||
|
||||
this.renderBanner();
|
||||
}
|
||||
|
||||
triggerEvent(status) {
|
||||
const eventName = status === 'accepted' ? 'cookieAccepted' : 'cookieRefused';
|
||||
window.dispatchEvent(new CustomEvent(eventName, {
|
||||
detail: { status, timestamp: new Date().toISOString() }
|
||||
}));
|
||||
}
|
||||
|
||||
// Le badge affiche maintenant une couleur selon le statut
|
||||
renderTrigger(status) {
|
||||
const colorClass = status === 'accepted' ? 'border-green-500' : 'border-red-500';
|
||||
const shadowClass = status === 'accepted' ? 'shadow-green-100' : 'shadow-red-100';
|
||||
|
||||
this.innerHTML = `
|
||||
<button id="cookie-reopen" title="Modifier vos préférences cookies"
|
||||
class="fixed bottom-4 right-4 bg-white border-2 ${colorClass} ${shadowClass} p-3 rounded-full shadow-lg hover:scale-110 transition-all z-40 text-2xl duration-300 flex items-center justify-center">
|
||||
<span class="relative flex h-3 w-3 mr-1">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full ${status === 'accepted' ? 'bg-green-400' : 'bg-red-400'} opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 ${status === 'accepted' ? 'bg-green-500' : 'bg-red-500'}"></span>
|
||||
</span>
|
||||
🍪
|
||||
</button>
|
||||
`;
|
||||
this.querySelector('#cookie-reopen').addEventListener('click', () => this.renderBanner());
|
||||
}
|
||||
|
||||
renderBanner() {
|
||||
this.innerHTML = `
|
||||
<div id="cookie-banner" class="fixed bottom-4 left-4 right-4 md:left-auto md:max-w-md z-50">
|
||||
<div class="bg-white border border-gray-200 rounded-xl shadow-2xl p-6 transition-all duration-300 transform scale-100 opacity-100">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||
🍪 Gestion des cookies
|
||||
</h3>
|
||||
|
||||
<p class="mt-2 text-sm text-gray-600 leading-relaxed">
|
||||
En acceptant les cookies, vous permettez au site d'activer des outils qui mesurent la <strong>vitesse du site</strong> et la <strong>liste des pages vues</strong>. Ces analyses nous permettent d'améliorer le site en continu. Ces données sont <strong>non revendues</strong> !
|
||||
</p>
|
||||
|
||||
<div class="mt-4 p-3 bg-gray-50 rounded-lg border border-gray-100">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="flex h-2 w-2 rounded-full bg-blue-500"></span>
|
||||
<span class="text-[10px] font-bold uppercase tracking-wider text-gray-700">Cookies techniques essentiels</span>
|
||||
</div>
|
||||
<p class="text-[12px] text-gray-500 leading-snug">
|
||||
Ces cookies sont <strong>obligatoires</strong> pour permettre la navigation, la sécurisation de vos accès et le processus de réservation. Ils ne peuvent pas être désactivés.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex flex-col sm:flex-row gap-3">
|
||||
<button id="btn-accept" class="flex-1 bg-slate-900 hover:bg-slate-800 text-white text-sm font-semibold py-2.5 px-4 rounded-lg transition-all active:scale-95">
|
||||
Accepter tout
|
||||
</button>
|
||||
<button id="btn-refuse" class="flex-1 bg-gray-100 hover:bg-gray-200 text-gray-700 text-sm font-medium py-2.5 px-4 rounded-lg transition-all">
|
||||
Refuser
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-center gap-4 text-[11px] text-gray-400">
|
||||
<a href="/reservation/cookies" class="underline hover:text-gray-600">Politique de cookies</a>
|
||||
<a href="/reservation/rgpd" class="underline hover:text-gray-600">Charte RGPD</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.querySelector('#btn-accept').addEventListener('click', () => this.updateStatus('accepted'));
|
||||
this.querySelector('#btn-refuse').addEventListener('click', () => this.updateStatus('refused'));
|
||||
}
|
||||
|
||||
updateStatus(status) {
|
||||
sessionStorage.setItem('ldk_cookie', status);
|
||||
|
||||
// Synchronisation avec le script Umami
|
||||
if (status === 'accepted') {
|
||||
localStorage.removeItem('umami.disabled');
|
||||
console.log("✅ Umami activé dans le localStorage");
|
||||
} else {
|
||||
localStorage.setItem('umami.disabled', '1');
|
||||
console.log("🛑 Umami désactivé dans le localStorage");
|
||||
}
|
||||
|
||||
// Déclenchement de tes Custom Events
|
||||
this.triggerEvent(status);
|
||||
|
||||
// Animation de sortie
|
||||
const banner = this.querySelector('#cookie-banner > div');
|
||||
if (banner) {
|
||||
banner.classList.add('opacity-0', 'translate-y-8', 'scale-95');
|
||||
setTimeout(() => {
|
||||
this.renderTrigger(status);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!customElements.get('cookie-banner')) {
|
||||
customElements.define('cookie-banner', CookieBanner);
|
||||
}
|
||||
@@ -1,19 +1,60 @@
|
||||
export class UtmAccount extends HTMLElement {
|
||||
connectedCallback() {
|
||||
async connectedCallback() {
|
||||
// 1. Vérification du consentement
|
||||
if (sessionStorage.getItem('ldk_cookie') !== 'accepted') return;
|
||||
|
||||
// 2. Vérification de la présence d'Umami (nécessaire si tu utilises l'objet global umami ailleurs)
|
||||
if (typeof umami === 'undefined') {
|
||||
console.warn('Umami script non détecté.');
|
||||
return;
|
||||
}
|
||||
const umamiScript = document.querySelector('script[data-website-id]');
|
||||
const websiteId = umamiScript ? umamiScript.getAttribute('data-website-id') : null;
|
||||
umami.identify('user_'+this.getAttribute('id'), { name: this.getAttribute('name'), email: this.getAttribute('email') });
|
||||
|
||||
const userId = this.getAttribute('id');
|
||||
const userName = this.getAttribute('name');
|
||||
const userEmail = this.getAttribute('email');
|
||||
|
||||
try {
|
||||
// 3. Envoi de l'identification à ton instance Umami (Tools Security)
|
||||
const response = await fetch("https://tools-security.esy-web.dev/api/send", {
|
||||
method: "POST",
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
"type": "identify",
|
||||
"payload": {
|
||||
"website": "bc640e0d-43fb-4c3a-bb17-1ac01cec9643",
|
||||
"screen": `${window.screen.width}x${window.screen.height}`,
|
||||
"language": navigator.language,
|
||||
"title": document.title,
|
||||
"hostname": window.location.hostname,
|
||||
"url": window.location.pathname,
|
||||
"referrer": document.referrer,
|
||||
"id": `user_${userId}`,
|
||||
"data": { "name": userName, "email": userEmail }
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const sessionId = result.sessionId;
|
||||
|
||||
// 4. Envoi du sessionId à ton backend Symfony
|
||||
if (sessionId) {
|
||||
await fetch('/reservation/umami', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ umami_session: sessionId })
|
||||
});
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class UtmEvent extends HTMLElement {
|
||||
connectedCallback() {
|
||||
// On attend un court instant pour s'assurer qu'umami est chargé
|
||||
// ou on vérifie s'il existe déjà
|
||||
// On ne tracke que si les cookies sont acceptés
|
||||
if (sessionStorage.getItem('ldk_cookie') !== 'accepted') return;
|
||||
|
||||
if (typeof umami === 'undefined') {
|
||||
console.warn('Umami script non détecté.');
|
||||
return;
|
||||
@@ -22,7 +63,7 @@ export class UtmEvent extends HTMLElement {
|
||||
const event = this.getAttribute('event');
|
||||
const dataRaw = this.getAttribute('data');
|
||||
|
||||
// Extraction dynamique du website-id depuis le script existant
|
||||
// Extraction dynamique du website-id
|
||||
const umamiScript = document.querySelector('script[data-website-id]');
|
||||
const websiteId = umamiScript ? umamiScript.getAttribute('data-website-id') : null;
|
||||
|
||||
@@ -32,11 +73,11 @@ export class UtmEvent extends HTMLElement {
|
||||
}
|
||||
|
||||
try {
|
||||
if (event == "click_pdf_product") {
|
||||
if (event === "click_pdf_product") {
|
||||
const data = JSON.parse(dataRaw);
|
||||
umami.track({
|
||||
website: websiteId,
|
||||
name:'Téléchargement document produit',
|
||||
name: 'Téléchargement document produit',
|
||||
data: data
|
||||
});
|
||||
}
|
||||
@@ -52,12 +93,9 @@ export class UtmEvent extends HTMLElement {
|
||||
|
||||
if (event === "view_product" && dataRaw) {
|
||||
const data = JSON.parse(dataRaw);
|
||||
|
||||
// Umami track accepte soit un nom seul,
|
||||
// soit un objet complet pour des propriétés personnalisées
|
||||
umami.track({
|
||||
website: websiteId,
|
||||
name:'Affichage produit',
|
||||
name: 'Affichage produit',
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user