```
✨ feat(assets/app.js): Ajoute gestion bandeau cookies et notifications en français.
Ajoute les messages, la logique et l'affichage des bandeaux de cookies et de notifications.
```
This commit is contained in:
180
assets/app.js
180
assets/app.js
@@ -3,9 +3,66 @@ import * as Turbo from "@hotwired/turbo"
|
||||
|
||||
import {PaymentForm} from './PaymentForm'
|
||||
|
||||
// --- CLÉ VAPID PUBLIQUE DU SERVEUR ---
|
||||
// Cette clé est nécessaire pour identifier notre application auprès du service push.
|
||||
// --- CLÉS DE STOCKAGE ET VAPID ---
|
||||
const VAPID_PUBLIC_KEY = "BKz0kdcsG6kk9KxciPpkfP8kEDAd408inZecij5kBDbQ1ZGZSNwS4KZ8FerC28LFXvgSqpDXtor3ePo0zBCdNqo";
|
||||
const COOKIE_STORAGE_KEY = 'cookies_accepted'; // Clé pour le consentement des cookies
|
||||
|
||||
|
||||
// --- MESSAGES ET TRADUCTIONS (fr et en) ---
|
||||
const MESSAGES = {
|
||||
fr: {
|
||||
// Notifications
|
||||
notificationTitle: "🔔 Activer les notifications",
|
||||
notificationText: "Recevez les nouvelles, les promotions et les événements de l'association.",
|
||||
notificationButton: "Activer",
|
||||
notificationClose: "Fermer la notification",
|
||||
|
||||
// Cookies
|
||||
cookieText: 'Ce site utilise uniquement des cookies de <strong>fonctionnement</strong> et de <strong>sécurité</strong> (e.g. Cloudflare) essentiels pour son bon fonctionnement. Aucune donnée personnelle n\'est collectée.',
|
||||
cookieLink: "Politique de cookies",
|
||||
cookieButton: "Accepter",
|
||||
},
|
||||
en: {
|
||||
// Notifications
|
||||
notificationTitle: "🔔 Enable Notifications",
|
||||
notificationText: "Receive news, promotions, and association events.",
|
||||
notificationButton: "Enable",
|
||||
notificationClose: "Close notification",
|
||||
|
||||
// Cookies
|
||||
cookieText: 'This website only uses <strong>functional</strong> and <strong>security</strong> cookies (e.g. Cloudflare) essential for its proper operation. No personal data is collected.',
|
||||
cookieLink: "Cookie Policy",
|
||||
cookieButton: "Accept",
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Détermine la langue à utiliser, en privilégiant l'attribut 'lang' du document HTML,
|
||||
* puis la langue du navigateur. Retourne l'ensemble de messages correspondant.
|
||||
* @returns {object} L'objet de messages pour la langue sélectionnée.
|
||||
*/
|
||||
function getLanguageMessages() {
|
||||
let langCode = 'fr'; // Français par défaut
|
||||
|
||||
// 1. Tente de lire la langue depuis l'attribut 'lang' du tag <html>
|
||||
const htmlLang = document.documentElement.lang;
|
||||
if (htmlLang) {
|
||||
// On prend les deux premiers caractères (ex: 'en-US' devient 'en')
|
||||
const normalizedHtmlLang = htmlLang.toLowerCase().substring(0, 2);
|
||||
if (normalizedHtmlLang === 'en') {
|
||||
langCode = 'en';
|
||||
}
|
||||
} else {
|
||||
// 2. Si l'attribut 'lang' est manquant, utilise la langue du navigateur
|
||||
const userLang = navigator.language || navigator.userLanguage;
|
||||
if (userLang && userLang.toLowerCase().startsWith('en')) {
|
||||
langCode = 'en';
|
||||
}
|
||||
}
|
||||
|
||||
return MESSAGES[langCode];
|
||||
}
|
||||
// --- FIN MESSAGES ET TRADUCTIONS ---
|
||||
|
||||
|
||||
/**
|
||||
@@ -48,8 +105,7 @@ function toggleMenu(button, menu) {
|
||||
* 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.
|
||||
// Réinitialisation des états des menus cachés après un chargement Turbo
|
||||
document.querySelectorAll('#mobile-menu, #userMenuDesktop, #userMenuMobile').forEach(menu => {
|
||||
if (!menu.classList.contains('hidden')) {
|
||||
menu.classList.add('hidden');
|
||||
@@ -105,8 +161,7 @@ function initializeUI() {
|
||||
// Simuler un panier non-vide au chargement (Mettre 0 pour un panier vide réel)
|
||||
updateCartDisplay(0);
|
||||
|
||||
// --- 4. Vérification de l'abonnement push au chargement (Logique demandée) ---
|
||||
// Si la permission est déjà accordée, nous vérifions si l'abonnement est enregistré.
|
||||
// --- 4. Vérification de l'abonnement push au chargement ---
|
||||
if ('Notification' in window && Notification.permission === 'granted') {
|
||||
subscribeAndSave();
|
||||
}
|
||||
@@ -201,22 +256,20 @@ async function promptForPermissionAndSubscribe() {
|
||||
* N'affiche QUE si la permission n'est PAS accordée.
|
||||
*/
|
||||
function handleNotificationBanner() {
|
||||
// Clé pour éviter de ré-afficher le bandeau si l'utilisateur vient de le fermer/cliquer.
|
||||
const BANNER_ID = 'notification-prompt-banner';
|
||||
const DURATION_MS = 15000; // 15 secondes d'affichage
|
||||
const M = getLanguageMessages(); // Récupère les messages traduits
|
||||
|
||||
// 1. NE PAS AFFICHER si la permission est déjà accordée (la vérification silencieuse est dans initializeUI)
|
||||
// 1. NE PAS AFFICHER si la permission est déjà accordée
|
||||
if ('Notification' in window && Notification.permission === 'granted') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Si le bandeau existe déjà (e.g. navigation rapide), on quitte.
|
||||
// 2. Si le bandeau existe déjà, on quitte.
|
||||
if (document.getElementById(BANNER_ID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// --- LA LOGIQUE DE CONTRÔLE DE TEMPS (LOCAL STORAGE) A ÉTÉ RETIRÉE ICI ---
|
||||
|
||||
// 3. Créer le conteneur du message
|
||||
const banner = document.createElement('div');
|
||||
banner.id = BANNER_ID;
|
||||
@@ -229,21 +282,21 @@ function handleNotificationBanner() {
|
||||
banner.innerHTML = `
|
||||
<div class="flex items-start justify-between">
|
||||
<p class="font-semibold text-sm leading-snug">
|
||||
🔔 Activer les notifications
|
||||
${M.notificationTitle}
|
||||
</p>
|
||||
<button id="closeNotificationBanner"
|
||||
aria-label="Fermer la notification"
|
||||
aria-label="${M.notificationClose}"
|
||||
class="ml-3 -mt-1 p-1 rounded-full text-indigo-200 hover:text-white hover:bg-indigo-700 transition">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x"><path d="M18 6L6 18"/><path d="M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-indigo-200">
|
||||
Recevez les nouvelles, les promotions et les événements de l'association.
|
||||
${M.notificationText}
|
||||
</p>
|
||||
<button id="activateNotifications"
|
||||
class="mt-3 w-full text-center py-2 bg-white text-indigo-600 font-bold text-sm rounded-lg
|
||||
shadow hover:bg-gray-100 transition transform hover:scale-[1.02]">
|
||||
Activer
|
||||
${M.notificationButton}
|
||||
</button>
|
||||
`;
|
||||
|
||||
@@ -287,21 +340,106 @@ function handleNotificationBanner() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Affiche le bandeau de consentement aux cookies en bas à droite s'il n'a jamais été accepté.
|
||||
*/
|
||||
function handleCookieBanner() {
|
||||
const BANNER_ID = 'cookie-banner';
|
||||
const M = getLanguageMessages(); // Récupère les messages traduits
|
||||
|
||||
// 1. NE PAS AFFICHER si le consentement est déjà enregistré
|
||||
if (localStorage.getItem(COOKIE_STORAGE_KEY) === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Si le bandeau existe déjà, on quitte.
|
||||
if (document.getElementById(BANNER_ID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Créer le conteneur du message (position bottom-right)
|
||||
const banner = document.createElement('div');
|
||||
banner.id = BANNER_ID;
|
||||
banner.className = `fixed bottom-4 right-4 z-50 p-4 max-w-sm
|
||||
bg-white text-gray-800 rounded-xl shadow-2xl
|
||||
transition-all duration-500 transform
|
||||
opacity-0 translate-y-full
|
||||
border border-gray-100
|
||||
md:right-8 md:bottom-8`; // Style initial (masqué)
|
||||
|
||||
// Utilisation de M.cookieText, M.cookieLink et M.cookieButton
|
||||
banner.innerHTML = `
|
||||
<p class="text-sm leading-relaxed">
|
||||
${M.cookieText}
|
||||
</p>
|
||||
<div class="mt-4 flex justify-end items-center">
|
||||
<a href="/cookies" class="text-xs font-medium text-indigo-600 hover:text-indigo-800 transition mr-4">
|
||||
${M.cookieLink}
|
||||
</a>
|
||||
<button id="acceptCookies"
|
||||
class="py-2 px-4 bg-indigo-600 text-white font-bold text-sm rounded-lg
|
||||
shadow-md hover:bg-indigo-700 transition transform hover:scale-[1.02]">
|
||||
${M.cookieButton}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(banner);
|
||||
|
||||
// 4. Fonctions d'animation et de gestion
|
||||
const hideBanner = () => {
|
||||
// Déclenche l'animation de disparition
|
||||
banner.classList.remove('opacity-100', 'translate-y-0');
|
||||
banner.classList.add('opacity-0', 'translate-y-full');
|
||||
// Supprime après l'animation
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(banner)) {
|
||||
document.body.removeChild(banner);
|
||||
}
|
||||
}, 600);
|
||||
};
|
||||
|
||||
// Clic sur le bouton d'acceptation
|
||||
document.getElementById('acceptCookies').addEventListener('click', () => {
|
||||
localStorage.setItem(COOKIE_STORAGE_KEY, 'true');
|
||||
hideBanner();
|
||||
});
|
||||
|
||||
// 5. Affichage
|
||||
// Montre le bandeau (déclencher l'animation)
|
||||
setTimeout(() => {
|
||||
banner.classList.remove('opacity-0', 'translate-y-full');
|
||||
banner.classList.add('opacity-100', 'translate-y-0');
|
||||
}, 100);
|
||||
}
|
||||
|
||||
|
||||
// --- INITIALISATION DES COMPOSANTS APRÈS TURBO/CHARGEMENT ---
|
||||
document.addEventListener('DOMContentLoaded', ()=>{
|
||||
customElements.define('payment-don',PaymentForm,{extends:'form'})
|
||||
|
||||
// initializeUI appelle subscribeAndSave si la permission est accordée
|
||||
initializeUI()
|
||||
if (typeof navigator.serviceWorker !== 'undefined') {
|
||||
// Assurez-vous que le Service Worker est bien enregistré en mode prod
|
||||
navigator.serviceWorker.register('sw.js')
|
||||
}
|
||||
// handleNotificationBanner n'affiche que si la permission n'est PAS accordée
|
||||
|
||||
// Gère le bandeau de notification (bottom-left)
|
||||
handleNotificationBanner()
|
||||
|
||||
// Gère le bandeau de cookies (bottom-right)
|
||||
handleCookieBanner()
|
||||
|
||||
const env = document.querySelector('meta[name="env"]')
|
||||
if(env.getAttribute('content') == "prod") {
|
||||
if (typeof navigator.serviceWorker !== 'undefined') {
|
||||
// Assurez-vous que le Service Worker est bien enregistré en mode prod
|
||||
navigator.serviceWorker.register('sw.js')
|
||||
}
|
||||
}
|
||||
});
|
||||
document.addEventListener('turbo:load', () => {
|
||||
initializeUI();
|
||||
handleNotificationBanner();
|
||||
handleCookieBanner(); // Doit être appelé à chaque chargement Turbo
|
||||
});
|
||||
document.addEventListener('turbo:load', initializeUI);
|
||||
|
||||
|
||||
// ====================================================================
|
||||
|
||||
BIN
public/assets/partenair/cosplay-familly.jpg
Normal file
BIN
public/assets/partenair/cosplay-familly.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
BIN
public/assets/partenair/couronnes-d-or.jpg
Normal file
BIN
public/assets/partenair/couronnes-d-or.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
public/assets/partenair/house-of-geek.jpg
Normal file
BIN
public/assets/partenair/house-of-geek.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -106,7 +106,60 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- SECTION 4: APPEL À L'ADHÉSION (CTA) --- #}
|
||||
{# --- SECTION 4: PARTENAIRES --- #}
|
||||
<div class="bg-white py-16">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="lg:text-center mb-12">
|
||||
<h2 class="text-base text-indigo-600 font-semibold tracking-wide uppercase">{{ 'home_partners.pretitle'|trans }}</h2>
|
||||
<p class="mt-2 text-3xl leading-8 font-extrabold tracking-tight text-gray-900 sm:text-4xl">
|
||||
{{ 'home_partners.title'|trans }}
|
||||
</p>
|
||||
<p class="mt-4 max-w-2xl text-xl text-gray-500 lg:mx-auto">
|
||||
{{ 'home_partners.subtitle'|trans }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{# Définition de la grille responsive pour la boucle #}
|
||||
<div class="mt-10 grid grid-cols-2 gap-8 md:grid-cols-6 lg:grid-cols-3">
|
||||
|
||||
{# BOUCLE SUR LES PARTENAIRES #}
|
||||
{# NOTE: La variable `partners` doit être passée par le contrôleur (e.g., [ { image: '/asset/partenair/house-of-geek.jpg', name: 'House Of Geek', facebook_link: 'lien' }, ... ] ) #}
|
||||
{% for partner in partners|default([
|
||||
{ 'image': '/assets/partenair/house-of-geek.jpg', 'name': 'House Of Geek', 'facebook_link': 'https://www.facebook.com/houseofgeek02' },
|
||||
{ 'image': '/assets/partenair/cosplay-familly.jpg', 'name': 'Cosplays family arts', 'facebook_link': 'https://www.facebook.com/profile.php?id=61568494078902' },
|
||||
{ 'image': '/assets/partenair/couronnes-d-or.jpg', 'name': 'Le Comité des Couronnes D’or', 'facebook_link': 'https://www.facebook.com/p/Le-Comit%C3%A9-des-Couronnes-Dor-61576548182126/' }
|
||||
]) %}
|
||||
{# Conteneur du partenaire #}
|
||||
<a href="{{ partner.facebook_link ?? '#' }}" target="_blank" class="col-span-1 flex flex-col items-center justify-center py-6 px-4 bg-white rounded-xl shadow-md border border-gray-100 hover:shadow-lg hover:border-indigo-200 transition duration-300 group">
|
||||
|
||||
{# Logo/Image du partenaire #}
|
||||
<img class="max-h-16 w-auto object-contain mb-3"
|
||||
src="{{ partner.image | imagine_filter('webp') }}"
|
||||
alt="{{ partner.name }}"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='block';"
|
||||
>
|
||||
|
||||
{# Fallback en texte si l'image ne se charge pas #}
|
||||
<span class="text-xl font-bold text-indigo-700 group-hover:text-indigo-900 transition duration-300">
|
||||
{{ partner.name }}
|
||||
</span>
|
||||
|
||||
{# Icône Facebook (Optionnelle) #}
|
||||
{% if partner.facebook_link is defined and partner.facebook_link %}
|
||||
<span class="text-sm text-gray-500 mt-2 flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-facebook mr-1"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"/></svg>
|
||||
Page Facebook
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
</a>
|
||||
{% endfor %}
|
||||
{# FIN BOUCLE PARTENAIRES #}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- SECTION 5: APPEL À L'ADHÉSION (CTA) --- #}
|
||||
<div class="bg-indigo-600">
|
||||
<div class="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:py-16 lg:px-8 lg:flex lg:items-center lg:justify-between">
|
||||
<h2 class="text-3xl font-extrabold tracking-tight text-white sm:text-4xl">
|
||||
|
||||
Reference in New Issue
Block a user