Location — Titre
EVJF & Evénements Workshops & Evénements
Réservation Événements — French Pole Riviera
Ateliers thématiques, stages intensifs et masterclass : plongez dans un univers pensé pour vous, quelle que soit votre expérience.
Que vous soyez du studio ou d'ailleurs, rejoignez une communauté passionnée pour partager, apprendre et progresser ensemble .
👤 Gérer / annuler ma réservation Déjà inscrit·e ? Retrouvez et gérez vos réservations ici. Sinon, découvrez nos événements ci-dessous pour réserver.
Chargement des événements…
Une question ?
En confirmant, vous recevrez un email récapitulatif. Votre place est garantie dès validation ✦
`;
}let events = [];
let selectedEventId = null;async function loadEvents() {
const container = document.getElementById('eventsContainer');
try {
// Requête simple sans index : on récupère TOUS les événements,
// puis on filtre et trie côté navigateur (plus rapide que de créer un index)
const snap = await getDocs(collection(db, "evenements"));// Compter le nombre de personnes en liste d'attente par événement
const waitlistCounts = {};
try {
const partSnap = await getDocs(query(
collection(db, "evenements_participants"),
where("statut", "==", "liste_attente")
));
partSnap.docs.forEach(d => {
const evId = d.data().evenement_id;
if (evId) waitlistCounts[evId] = (waitlistCounts[evId] || 0) + 1;
});
} catch (e) {
console.warn('Comptage liste attente impossible:', e);
}events = snap.docs
.map(d => ({ id: d.id, ...d.data(), _waitlistCount: waitlistCounts[d.id] || 0 }))
// Filtre : uniquement les événements actifs
.filter(e => e.actif === true)
// Filtre : masquer les événements passés (avec 24h de marge)
.filter(e => {
const endDate = e.date_fin || e.date_debut;
if (!endDate) return true;
const d = new Date(endDate.seconds ? endDate.seconds * 1000 : endDate);
return d.getTime() + 24*60*60*1000 > Date.now();
})
// Tri : par date de début croissante
.sort((a, b) => {
const dA = a.date_debut?.seconds ? a.date_debut.seconds : 0;
const dB = b.date_debut?.seconds ? b.date_debut.seconds : 0;
return dA - dB;
});if (events.length === 0) {
container.innerHTML = `
`;
return;
}// Si 1 seul événement → pas de carrousel (affichage simple)
if (events.length === 1) {
container.innerHTML = `
`;
attachCardListeners();
} else {
// Plusieurs événements → carrousel avec autoplay
container.innerHTML = `
`;
attachCardListeners();
initCarousel();
}
} catch (err) {
console.error(err);
container.innerHTML = `
`;
}
}// ⏰ Les réservations ferment 4h avant le DÉBUT de l'événement
const RESERVATION_CLOSE_HOURS = 4;
function isReservationClosed(evt) {
if (!evt.date_debut) return false;
// Date de début
const debut = evt.date_debut.seconds
? new Date(evt.date_debut.seconds * 1000)
: new Date(evt.date_debut);
// Appliquer l'heure de début si disponible
if (evt.heure_debut) {
const [h, m] = evt.heure_debut.split(':').map(Number);
debut.setHours(h || 0, m || 0, 0, 0);
}
// Limite = début - 4h
const limite = debut.getTime() - RESERVATION_CLOSE_HOURS * 60 * 60 * 1000;
return Date.now() >= limite;
}function renderCard(evt) {
const isExterne = !!evt.lien_billetterie;
const placesRestantes = (evt.places_max || 0) - (evt.places_reservees || 0);
const isFull = !isExterne && placesRestantes <= 0;
const isLimited = !isExterne && placesRestantes > 0 && placesRestantes <= 3;// ⏰ Réservations fermées 6h avant le début de l'événement (ignoré si billetterie externe)
const reservationsClosed = !isExterne && isReservationClosed(evt);// Gestion liste d'attente
const maxWaitlist = (evt.max_liste_attente !== undefined && evt.max_liste_attente !== null) ? evt.max_liste_attente : 4;
const waitlistCount = evt._waitlistCount || 0;
const waitlistFull = isFull && (maxWaitlist === 0 || waitlistCount >= maxWaitlist);
const canJoinWaitlist = isFull && !waitlistFull;const dateStr = formatDate(evt.date_debut, evt.date_fin);
const horaires = (evt.heure_debut && evt.heure_fin) ? `${evt.heure_debut} - ${evt.heure_fin}` : '';
const niveauInfo = formatNiveau(evt.niveau);// Badge de statut
let statusBadge;
if (isExterne) {
statusBadge = `
`;
}// Bouton selon l'état
let btnHTML;
if (evt.lien_billetterie) {
// Billetterie externe : redirige vers la boutique (nouvel onglet)
btnHTML = `
`;
}const notClickable = !isExterne && (waitlistFull || reservationsClosed);
const cardClass = notClickable ? 'is-full' : (canJoinWaitlist ? 'is-full is-waitlist' : '');return `
`;
}// Formater le niveau d'un événement (icône + label)
function formatNiveau(niveau) {
const levels = {
'tous_niveaux': { icon: '🌟', label: 'Tous niveaux' },
'debutant': { icon: '🌱', label: 'Débutant' },
'intermediaire': { icon: '🔥', label: 'Intermédiaire' },
'avance': { icon: '💎', label: 'Avancé' },
'pro': { icon: '👑', label: 'Pro' }
};
return levels[niveau] || null;
}// Ajoute automatiquement le € si la valeur est un simple nombre
// "30" → "30€", "30€" → "30€", "Sur demande" → "Sur demande"
// Extraire un nombre depuis une string de prix ("15€" → 15, "15.50" → 15.5, "Sur demande" → 0)
function parsePrix(prix) {
if (typeof prix === 'number') return prix;
if (!prix) return 0;
const match = String(prix).replace(',', '.').match(/[\d.]+/);
return match ? parseFloat(match[0]) : 0;
}function formatPrix(prix) {
if (!prix) return '';
const str = String(prix).trim();
// Si ça contient uniquement chiffres, virgule, point, espace → ajouter €
if (/^[\d\s,.]+$/.test(str)) {
return str + '€';
}
return str;
}function formatTarifsShort(tarifs) {
if (!tarifs || !Array.isArray(tarifs) || tarifs.length === 0) return '';
if (tarifs.length === 1) return formatPrix(tarifs[0].prix);
const prix = tarifs.map(t => parseFloat(String(t.prix).replace(/[^\d.,]/g, '').replace(',', '.'))).filter(n => !isNaN(n));
if (prix.length > 0) {
const min = Math.min(...prix);
return `À partir de ${min}€`;
}
return `${tarifs.length} tarifs`;
}function attachCardListeners() {
// Clic sur le bouton "Infos & Réservation" ou "Liste d'attente"
document.querySelectorAll('.event-cta, .card-cta').forEach(btn => {
if (btn.disabled) return; // Bouton "Complet" désactivé
if (btn.dataset.external === '1') return; // Lien billetterie externe : comportement natif du
btn.addEventListener('click', (e) => {
e.stopPropagation();
openEventDetail(btn.dataset.eventId);
});
});
// Clic sur la carte entière (sauf si liste d'attente pleine)
document.querySelectorAll('.fpr-event-card, .fpr-card[data-id]').forEach(card => {
if (card.dataset.waitlistFull === '1') return; // Carte complète non cliquable
card.addEventListener('click', () => {
// Si billetterie externe : ouvrir le lien au lieu du modal
const lien = card.dataset.billetterie;
if (lien) { window.open(lien, '_blank', 'noopener,noreferrer'); return; }
openEventDetail(card.dataset.id);
});
});
}// ═══════════════════════════════════════════════════════════════
// CARROUSEL — autoplay avec pause au survol, flèches, dots, swipe
// ═══════════════════════════════════════════════════════════════const CAROUSEL_AUTOPLAY_MS = 6000; // 6 secondes par slidelet carouselState = {
currentIndex: 0,
total: 0,
autoplayTimer: null,
progressTimer: null,
progressStart: 0,
isPaused: false,
track: null,
dots: null,
progress: null,
counter: null,
prevBtn: null,
nextBtn: null
};function initCarousel() {
const carousel = document.getElementById('fprCarousel');
if (!carousel) return;carouselState.track = document.getElementById('fprTrack');
carouselState.dots = document.getElementById('fprDots');
carouselState.progress = document.getElementById('fprProgress');
carouselState.counter = document.getElementById('fprCounterCurrent');
carouselState.prevBtn = carousel.querySelector('.fpr-carousel-arrow.prev');
carouselState.nextBtn = carousel.querySelector('.fpr-carousel-arrow.next');
carouselState.total = events.length;
carouselState.currentIndex = 0;// Flèches
carouselState.prevBtn.addEventListener('click', (e) => {
e.stopPropagation();
goToSlide(carouselState.currentIndex - 1);
restartAutoplay();
});
carouselState.nextBtn.addEventListener('click', (e) => {
e.stopPropagation();
goToSlide(carouselState.currentIndex + 1);
restartAutoplay();
});// Dots
carouselState.dots.querySelectorAll('.fpr-carousel-dot').forEach(dot => {
dot.addEventListener('click', (e) => {
e.stopPropagation();
const idx = parseInt(dot.dataset.index, 10);
goToSlide(idx);
restartAutoplay();
});
});// Pause au survol
carousel.addEventListener('mouseenter', pauseAutoplay);
carousel.addEventListener('mouseleave', resumeAutoplay);// Pause au focus (accessibilité clavier)
carousel.addEventListener('focusin', pauseAutoplay);
carousel.addEventListener('focusout', resumeAutoplay);// Support swipe tactile
let touchStartX = 0;
let touchEndX = 0;
const trackWrap = carousel.querySelector('.fpr-carousel-track-wrap');
trackWrap.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
pauseAutoplay();
}, { passive: true });
trackWrap.addEventListener('touchend', (e) => {
touchEndX = e.changedTouches[0].screenX;
const diff = touchStartX - touchEndX;
if (Math.abs(diff) > 50) {
if (diff > 0) goToSlide(carouselState.currentIndex + 1);
else goToSlide(carouselState.currentIndex - 1);
}
restartAutoplay();
}, { passive: true });// Navigation clavier
document.addEventListener('keydown', (e) => {
// Uniquement si la modal n'est pas ouverte
const modalOpen = document.querySelector('.fpr-modal-full.open');
if (modalOpen) return;
if (e.key === 'ArrowLeft') {
goToSlide(carouselState.currentIndex - 1);
restartAutoplay();
} else if (e.key === 'ArrowRight') {
goToSlide(carouselState.currentIndex + 1);
restartAutoplay();
}
});// Pause si l'onglet est en arrière-plan (éco-énergie)
document.addEventListener('visibilitychange', () => {
if (document.hidden) pauseAutoplay();
else resumeAutoplay();
});// Démarrer autoplay
startAutoplay();// Égaliser la hauteur de toutes les slides (après rendu complet)
setTimeout(equalizeSlideHeights, 100);
// Recalculer au resize
window.addEventListener('resize', () => {
clearTimeout(window._fprResizeTimer);
window._fprResizeTimer = setTimeout(equalizeSlideHeights, 150);
});
}// Égalise la hauteur de toutes les cartes du carrousel sur la plus haute
function equalizeSlideHeights() {
const cards = document.querySelectorAll('.fpr-carousel-slide .fpr-event-card');
if (cards.length === 0) return;// Reset d'abord pour recalculer les hauteurs naturelles
cards.forEach(c => c.style.minHeight = 'auto');// Sur mobile : on laisse chaque carte à sa taille naturelle (pas d'égalisation)
// car ça créerait des cartes trop grandes sur petit écran
const isMobile = window.innerWidth <= 768;
if (isMobile) return;// Desktop : trouver la plus haute
let maxHeight = 0;
cards.forEach(c => {
const h = c.offsetHeight;
if (h > maxHeight) maxHeight = h;
});// Appliquer la hauteur max à toutes
if (maxHeight > 0) {
cards.forEach(c => c.style.minHeight = maxHeight + 'px');
}
}function goToSlide(index) {
const total = carouselState.total;
if (total === 0) return;
// Boucle infinie
if (index < 0) index = total - 1;
if (index >= total) index = 0;
carouselState.currentIndex = index;// Déplacer le track
carouselState.track.style.transform = `translateX(-${index * 100}%)`;// Mettre à jour les dots
carouselState.dots.querySelectorAll('.fpr-carousel-dot').forEach((dot, i) => {
dot.classList.toggle('active', i === index);
});// Compteur
if (carouselState.counter) carouselState.counter.textContent = index + 1;
}function startAutoplay() {
stopAutoplay();
if (carouselState.total <= 1) return;
carouselState.progressStart = Date.now();// Animation progression visuelle
carouselState.progress.style.width = '0%';
carouselState.progressTimer = setInterval(() => {
if (carouselState.isPaused) return;
const elapsed = Date.now() - carouselState.progressStart;
const pct = Math.min((elapsed / CAROUSEL_AUTOPLAY_MS) * 100, 100);
carouselState.progress.style.width = pct + '%';
}, 50);// Timer de changement de slide
carouselState.autoplayTimer = setTimeout(() => {
if (!carouselState.isPaused) {
goToSlide(carouselState.currentIndex + 1);
startAutoplay();
}
}, CAROUSEL_AUTOPLAY_MS);
}function stopAutoplay() {
if (carouselState.autoplayTimer) {
clearTimeout(carouselState.autoplayTimer);
carouselState.autoplayTimer = null;
}
if (carouselState.progressTimer) {
clearInterval(carouselState.progressTimer);
carouselState.progressTimer = null;
}
}function pauseAutoplay() {
carouselState.isPaused = true;
}function resumeAutoplay() {
if (!carouselState.isPaused) return;
carouselState.isPaused = false;
// Relancer l'autoplay à partir de maintenant (pas de saut)
restartAutoplay();
}function restartAutoplay() {
stopAutoplay();
if (!carouselState.isPaused) startAutoplay();
}// ═══════════════════════════════════════════════════════════════
// MODAL DÉTAIL + RÉSERVATION — nouveau flow
// ═══════════════════════════════════════════════════════════════let currentStep = 1;
let isWaitlistMode = false;// Ouverture de la modal en mode découverte (détail + infos)
// 🎟️ MODE SPECTACLE — Gestion des compteurs de billets
window.billetCounts = [];window.changeBilletCount = function(idx, delta) {
if (!window.billetCounts) return;
const maxResa = parseInt(document.getElementById('spectacleMaxResa')?.value, 10) || 6;
const currentTotal = window.billetCounts.reduce((a, b) => a + b, 0);
const newCount = (window.billetCounts[idx] || 0) + delta;// Empêcher de descendre sous 0
if (newCount < 0) return;// Empêcher de dépasser le max
if (delta > 0 && currentTotal >= maxResa) {
showToast(`Maximum ${maxResa} billets par réservation`);
return;
}window.billetCounts[idx] = newCount;
document.getElementById('billetCount' + idx).textContent = newCount;// Ajouter classe "actif" si > 0
const row = document.querySelector(`.spectacle-tarif-row[data-idx="${idx}"]`);
if (row) {
row.classList.toggle('has-billets', newCount > 0);
}updateSpectacleTotal();
};function updateSpectacleTotal() {
if (!window.billetCounts) return;
let totalPlaces = 0;
let totalMontant = 0;document.querySelectorAll('.spectacle-tarif-row').forEach(row => {
const idx = parseInt(row.dataset.idx, 10);
const prix = parseFloat(row.dataset.prix) || 0;
const count = window.billetCounts[idx] || 0;
totalPlaces += count;
totalMontant += count * prix;
});const placesEl = document.getElementById('totalPlaces');
const montantEl = document.getElementById('totalMontant');
if (placesEl) placesEl.textContent = totalPlaces + (totalPlaces > 1 ? ' places' : ' place');
if (montantEl) montantEl.textContent = totalMontant + ' €';// Stocker dans hidden input pour la soumission
const billetsInput = document.getElementById('spectacleBillets');
if (billetsInput) billetsInput.value = JSON.stringify(window.billetCounts);
}window.openEventDetail = function(id) {
selectedEventId = id;
reservationProcessed = false; // Reset flag pour permettre une nouvelle résa
const evt = events.find(e => e.id === id);
if (!evt) return;// Événement à billetterie externe : rediriger au lieu d'ouvrir le formulaire interne
if (evt.lien_billetterie) {
window.open(evt.lien_billetterie, '_blank', 'noopener,noreferrer');
return;
}// Sécurité : bloquer si réservations closes (< 6h avant)
if (isReservationClosed(evt)) {
showToast("Les réservations sont closes pour cet événement (fermeture 4h avant).");
return;
}// Sécurité : bloquer si liste d'attente pleine
const placesMaxCheck = parseInt(evt.places_max, 10) || 0;
const placesReserveesCheck = parseInt(evt.places_reservees, 10) || 0;
const isFullCheck = placesMaxCheck > 0 && (placesMaxCheck - placesReserveesCheck) <= 0;
const maxWaitlistCheck = (evt.max_liste_attente !== undefined && evt.max_liste_attente !== null) ? evt.max_liste_attente : 4;
const waitlistCountCheck = evt._waitlistCount || 0;
if (isFullCheck && (maxWaitlistCheck === 0 || waitlistCountCheck >= maxWaitlistCheck)) {
showToast("Cet événement est complet. La liste d'attente est pleine.");
return;
}// Déterminer si l'événement est en liste d'attente
// Double sécurité : on vérifie places_max ET places_reservees sont des nombres valides
const placesMax = parseInt(evt.places_max, 10) || 0;
const placesReservees = parseInt(evt.places_reservees, 10) || 0;
const placesRestantes = placesMax - placesReservees;
isWaitlistMode = placesMax > 0 && placesRestantes <= 0;console.log('🎯 Ouverture event:', evt.titre, '| Places:', placesReservees, '/', placesMax, '| Waitlist:', isWaitlistMode);// Afficher ou masquer le bandeau liste d'attente
document.getElementById('waitlistBanner').style.display = isWaitlistMode ? 'flex' : 'none';// HERO (image ou vidéo)
const hero = document.getElementById('modalHero');
if (evt.video) {
hero.innerHTML = ` `;
} else if (evt.image) {
hero.innerHTML = ` `;
} else {
hero.innerHTML = '';
hero.style.display = 'none';
}// BADGES date/horaires/lieu
const dateStr = formatDate(evt.date_debut, evt.date_fin);
const horaires = (evt.heure_debut && evt.heure_fin) ? `${evt.heure_debut} - ${evt.heure_fin}` : '';
let placesBadge;
if (isWaitlistMode) {
placesBadge = `Complet `;
} else if (placesRestantes <= 3) {
placesBadge = `⚡ ${placesRestantes} place${placesRestantes > 1 ? 's' : ''} restante${placesRestantes > 1 ? 's' : ''} `;
} else {
placesBadge = `${placesRestantes} places `;
}// Badge niveau (si renseigné)
const niveauInfo = formatNiveau(evt.niveau);
const niveauBadge = niveauInfo
? `${niveauInfo.icon} ${escapeHtml(niveauInfo.label)} `
: '';document.getElementById('modalBadges').innerHTML = `
${escapeHtml(dateStr)}
${horaires ? `${escapeHtml(horaires)} ` : ''}
${placesBadge}
${niveauBadge}
📍 ${escapeHtml(evt.lieu || 'French Pole Riviera — Nice')}
`;// TITRE
document.getElementById('modalTitle').textContent = evt.titre;// NOTE IMPORTANTE (si renseignée)
const noteWrap = document.getElementById('modalNoteWrap');
if (evt.note_importante && evt.note_importante.trim()) {
document.getElementById('modalNoteText').textContent = evt.note_importante;
noteWrap.style.display = 'flex';
} else {
noteWrap.style.display = 'none';
}// DESCRIPTION (avec saut de ligne préservé grâce au CSS white-space: pre-line)
const descEl = document.getElementById('modalDescription');
if (evt.description && evt.description.trim()) {
descEl.textContent = evt.description;
descEl.style.display = 'block';
} else {
descEl.style.display = 'none';
}// TARIFS — RADIOS (Workshop) ou COMPTEURS (Spectacle)
const tarifOptions = document.getElementById('tarifOptions');
const stepTitle = document.querySelector('.step-panel[data-step="2"] .step-title');
const isSpectacle = evt.type === 'spectacle';if (isSpectacle && evt.tarifs && evt.tarifs.length > 0) {
// MODE SPECTACLE : compteurs multi-billets
const maxParResa = parseInt(evt.max_par_resa, 10) || 6;
if (stepTitle) stepTitle.textContent = `🎟️ Choisissez vos billets`;tarifOptions.innerHTML = `
Maximum ${maxParResa} billets par réservation
${evt.tarifs.map((t, i) => `${escapeHtml(t.libelle)}
${escapeHtml(formatPrix(t.prix))}
−
0
+
`).join('')}
`;
// Reset les compteurs
window.billetCounts = Array(evt.tarifs.length).fill(0);
updateSpectacleTotal();
} else if (evt.tarifs && Array.isArray(evt.tarifs) && evt.tarifs.length > 0) {
// MODE WORKSHOP : radios classiques
if (stepTitle) stepTitle.textContent = `Choisissez votre tarif`;
tarifOptions.innerHTML = evt.tarifs.map((t, i) => `
${escapeHtml(t.libelle)}
${escapeHtml(formatPrix(t.prix))}
`).join('');
} else {
if (stepTitle) stepTitle.textContent = `Choisissez votre tarif`;
tarifOptions.innerHTML = `
Inscription
Sur demande
`;
}// Ouverture de la modal directement sur l'étape 1 (Infos)
goToStep(1);
document.getElementById('eventModal').classList.add('open');
document.body.style.overflow = 'hidden';
};// Aller à une étape précise
function buildAccompagnantsFields() {
const wrap = document.getElementById('accompagnantsWrap');
if (!wrap) return;const isSpectacleMode = document.getElementById('spectacleBillets') !== null;
const total = (isSpectacleMode && window.billetCounts)
? window.billetCounts.reduce((a, b) => a + b, 0)
: 0;// Pas de spectacle ou un seul billet → pas de champs supplémentaires
if (!isSpectacleMode || total <= 1) {
wrap.style.display = 'none';
wrap.innerHTML = '';
return;
}// Préserver les valeurs déjà saisies si on revient sur l'étape
const anciens = Array.from(wrap.querySelectorAll('.accomp-input')).map(i => i.value);let html = `Nom des participant·es (${total} places)
Merci d'indiquer le prénom et le nom de chaque personne, vous compris·e.
`;for (let i = 0; i < total; i++) {
const val = anciens[i] || '';
const label = i === 0 ? 'Place 1 (vous)' : 'Place ' + (i + 1);
html += `
${label}
`;
}
html += `
`;
wrap.innerHTML = html;
wrap.style.display = 'block';// La 1re place reflète automatiquement le réservant (prénom + nom du haut)
syncFirstAccompagnant();
const pIn = document.getElementById('prenom');
const nIn = document.getElementById('nom');
if (pIn) pIn.oninput = syncFirstAccompagnant;
if (nIn) nIn.oninput = syncFirstAccompagnant;
}function syncFirstAccompagnant() {
const first = document.querySelector('#accompagnantsWrap .accomp-input[data-idx="0"]');
if (!first) return;
const prenom = (document.getElementById('prenom')?.value || '').trim();
const nom = (document.getElementById('nom')?.value || '').trim();
first.value = (prenom + ' ' + nom).trim();
}function goToStep(step) {
currentStep = step;// Mise à jour des panels
document.querySelectorAll('.step-panel').forEach(p => {
p.classList.toggle('active', parseInt(p.dataset.step, 10) === step);
});// Mise à jour du stepper visuel
document.querySelectorAll('.stepper-item').forEach(item => {
const n = parseInt(item.dataset.step, 10);
item.classList.remove('active', 'done');
if (n === step) item.classList.add('active');
else if (n < step) item.classList.add('done');
});// Mise à jour des barres entre étapes
document.querySelectorAll('.stepper-bar').forEach((bar, i) => {
bar.style.background = (i + 1 < step) ? 'var(--gold-3)' : 'var(--border)';
});// Boutons navigation
document.getElementById('btnPrev').style.display = step > 1 ? '' : 'none';
document.getElementById('btnNext').style.display = step < 4 ? '' : 'none';
document.getElementById('btnConfirm').style.display = step === 4 ? '' : 'none';// Générer les champs "noms des participants" à l'étape 3 (spectacle multi-billets)
if (step === 3) buildAccompagnantsFields();// Remplir le récap à l'étape 4
if (step === 4) {
fillRecap();// Adapter le banner + bouton agenda selon mode (waitlist ou non)
const banner = document.getElementById('recapStatusBanner');
const agendaBtn = document.getElementById('recapAgendaBtn');
const stepTitle = document.querySelector('.step-panel[data-step="4"] .step-title');if (isWaitlistMode) {
// Mode liste d'attente : pas de bouton agenda (place pas garantie)
if (stepTitle) stepTitle.innerHTML = "⏳ Inscription en liste d'attente";
if (banner) banner.innerHTML = "✉️ Un email de confirmation vient de vous être envoyé. Nous vous contacterons dès qu'une place se libère.";
if (agendaBtn) agendaBtn.style.display = 'none';
} else {
// Mode réservation confirmée : bouton agenda visible
if (stepTitle) stepTitle.innerHTML = "✓ Votre réservation est confirmée";
if (banner) banner.innerHTML = '✉️ Un email de confirmation vient de vous être envoyé avec tous les détails et un fichier .ics à ajouter à votre agenda.';
if (agendaBtn) agendaBtn.style.display = '';
}
}// Adapter le texte du bouton "Suivant" selon l'étape ET le mode (liste d'attente ou non)
const btnNext = document.getElementById('btnNext');
const btnConfirm = document.getElementById('btnConfirm');
if (isWaitlistMode) {
if (step === 1) btnNext.textContent = "S'inscrire en liste d'attente →";
else if (step === 2) btnNext.textContent = 'Continuer →';
else if (step === 3) btnNext.textContent = 'Confirmer mon inscription ⏳';
btnConfirm.textContent = "Fermer";
} else {
if (step === 1) btnNext.textContent = 'Je réserve →';
else if (step === 2) btnNext.textContent = 'Continuer →';
else if (step === 3) btnNext.textContent = 'Confirmer ma réservation ✦';
btnConfirm.textContent = "Fermer";
}// Scroll en haut de la modal
document.querySelector('.fpr-modal-shell').scrollTop = 0;
document.querySelector('.fpr-modal-full').scrollTop = 0;
}// Passer à l'étape suivante (avec validation)
window.nextStep = async function() {
// Étape 1 (Infos) : pas de validation, on continue juste
// Étape 2 : choix du tarif obligatoire (workshop) OU au moins 1 billet (spectacle)
if (currentStep === 2) {
// Mode SPECTACLE : vérifier qu'au moins 1 billet est sélectionné
const isSpectacleMode = document.getElementById('spectacleBillets') !== null;
if (isSpectacleMode) {
const totalBillets = (window.billetCounts || []).reduce((a, b) => a + b, 0);
if (totalBillets === 0) {
showToast("Sélectionnez au moins 1 billet");
return;
}
} else {
// Mode WORKSHOP : vérifier qu'un radio est coché
const tarifChecked = document.querySelector('input[name="tarif_choix"]:checked');
if (!tarifChecked) { showToast("Merci de choisir un tarif"); return; }
}
}
// Étape 3 : coordonnées obligatoires + ENREGISTREMENT + ENVOI MAIL
if (currentStep === 3) {
const prenom = document.getElementById('prenom').value.trim();
const nom = document.getElementById('nom').value.trim();
const email = document.getElementById('email').value.trim();
const telephone = document.getElementById('telephone').value.trim();
if (!prenom || !nom || !email || !telephone) {
showToast("Merci de remplir tous les champs"); return;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
showToast("Format d'email invalide"); return;
}// 🎯 NOUVEAU : Lance le processus de réservation MAINTENANT (étape 3 → 4)
const success = await processReservation();
if (!success) return; // Erreur → on reste sur l'étape 3
}
if (currentStep < 4) goToStep(currentStep + 1);
};// Revenir à l'étape précédente
window.prevStep = function() {
if (currentStep > 1) goToStep(currentStep - 1);
};// Remplir le récapitulatif à l'étape 3
function fillRecap() {
const evt = events.find(e => e.id === selectedEventId);
if (!evt) return;const prenom = document.getElementById('prenom').value.trim();
const nom = document.getElementById('nom').value.trim();
const email = document.getElementById('email').value.trim();
const telephone = document.getElementById('telephone').value.trim();const isSpectacleMode = document.getElementById('spectacleBillets') !== null;// Construire le détail des choix (workshop = 1 ligne / spectacle = N lignes)
let choixHTML = '';
let totalMontant = 0;
let totalPlaces = 0;if (isSpectacleMode && window.billetCounts) {
// Mode SPECTACLE : afficher chaque tarif avec sa quantité
evt.tarifs.forEach((t, i) => {
const count = window.billetCounts[i] || 0;
if (count > 0) {
const prixUnitaire = parsePrix(t.prix) || 0;
const sousTotal = count * prixUnitaire;
totalMontant += sousTotal;
totalPlaces += count;
choixHTML += `
${escapeHtml(t.libelle)} × ${count}
${sousTotal} €
`;
}
});
choixHTML += `
Total (${totalPlaces} place${totalPlaces > 1 ? 's' : ''})
${totalMontant} €
`;
} else {
// Mode WORKSHOP : 1 seul tarif
const tarifInput = document.querySelector('input[name="tarif_choix"]:checked');
const idx = tarifInput ? parseInt(tarifInput.value, 10) : 0;
const tarifChoisi = (evt.tarifs && evt.tarifs[idx]) ? evt.tarifs[idx] : { libelle: 'Inscription', prix: 'Sur demande' };
choixHTML = `
Votre choix
${escapeHtml(tarifChoisi.libelle)} — ${escapeHtml(formatPrix(tarifChoisi.prix))}
`;
}const horaires = (evt.heure_debut && evt.heure_fin) ? `${evt.heure_debut} - ${evt.heure_fin}` : '';document.getElementById('recapBox').innerHTML = `
Événement
${escapeHtml(evt.titre)}
Date
${escapeHtml(formatDate(evt.date_debut, evt.date_fin))}
${horaires ? `
Horaires
${escapeHtml(horaires)}
` : ''}
Lieu
${escapeHtml(evt.lieu || 'French Pole Riviera — Nice')}
${choixHTML}
Nom
${escapeHtml(prenom + ' ' + nom)}
Email
${escapeHtml(email)}
Téléphone
${escapeHtml(telephone)}
`;
}// Fermer la modal
window.closeEventModal = function() {
document.getElementById('eventModal').classList.remove('open');
document.body.style.overflow = '';
selectedEventId = null;
// Stopper toute vidéo en cours
const hero = document.getElementById('modalHero');
const vid = hero.querySelector('video');
if (vid) { vid.pause(); vid.src = ''; }
hero.innerHTML = '';
hero.style.display = '';
// Reset pour la prochaine ouverture
setTimeout(() => {
document.getElementById('reservationForm').reset();
restoreClientData(); // Re-remplit les coords si localStorage
goToStep(1);
}, 400);
};// Clic sur le fond = fermer (mais pas le contenu)
window.handleModalBgClick = function(e) {
if (e.target.id === 'eventModal') closeEventModal();
};// Pour compat : anciennes fonctions aliasées
window.unselectEvent = closeEventModal;// 🎯 FONCTION CENTRALE : Anti-doublon + Enregistrement Firebase + Envoi mail
// Retourne true si succès, false si erreur (la modal reste sur l'étape 3)
// Évite les double-appels grâce au flag global reservationProcessed
let reservationProcessed = false;async function processReservation() {
if (reservationProcessed) {
console.log('⚠️ Réservation déjà traitée — skip');
return true;
}if (!selectedEventId) { showToast("Veuillez sélectionner un événement"); return false; }const prenom = document.getElementById('prenom').value.trim();
const nom = document.getElementById('nom').value.trim();
const email = document.getElementById('email').value.trim();
const telephone = document.getElementById('telephone').value.trim();// Détection du mode (workshop ou spectacle)
const isSpectacleMode = document.getElementById('spectacleBillets') !== null;
let tarifChoixInput = null;
let nbBilletsTotaux = 1; // workshop = 1 place / spectacle = total billetsif (isSpectacleMode) {
// Mode SPECTACLE : vérifier qu'au moins 1 billet
nbBilletsTotaux = (window.billetCounts || []).reduce((a, b) => a + b, 0);
if (nbBilletsTotaux === 0) {
showToast("Sélectionnez au moins 1 billet");
return false;
}
} else {
// Mode WORKSHOP : vérifier qu'un radio est coché
tarifChoixInput = document.querySelector('input[name="tarif_choix"]:checked');
if (!tarifChoixInput) { showToast("Merci de choisir un tarif"); return false; }
}// Loader sur le bouton "Suivant" de l'étape 3
const btnNext = document.querySelector('.step-nav .fpr-cta:not(.outline)');
if (btnNext) {
btnNext.classList.add('is-loading');
btnNext.disabled = true;
}try {
// ✅ VÉRIFICATION ANTI-DOUBLON
const emailNormalized = email.toLowerCase().trim();
const dupQuery = query(
collection(db, "evenements_participants"),
where("evenement_id", "==", selectedEventId),
where("email", "==", emailNormalized)
);
const dupSnap = await getDocs(dupQuery);if (!dupSnap.empty) {
const existing = dupSnap.docs[0].data();
const dateRes = existing.date_reservation?.seconds
? new Date(existing.date_reservation.seconds * 1000).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })
: 'récemment';
const statutLabel = existing.statut === 'liste_attente' ? '⏳ Liste d\'attente' : '✅ Confirmée';showDuplicateModal({
prenom: existing.prenom || prenom,
statut: statutLabel,
dateInscription: dateRes,
email: emailNormalized
});
if (btnNext) {
btnNext.classList.remove('is-loading');
btnNext.disabled = false;
}
return false;
}
} catch (errCheck) {
console.error('Erreur vérification doublon :', errCheck);
// On continue (mieux vaut accepter qu'empêcher)
}try {
let evt;if (isWaitlistMode) {
const eventRef = doc(db, "evenements", selectedEventId);
const eventSnap = await getDoc(eventRef);
if (!eventSnap.exists()) throw new Error("Événement introuvable");
evt = { id: eventSnap.id, ...eventSnap.data() };// Double-check : s'il reste finalement des places → bascule en confirmé
const placesMaxFresh = parseInt(evt.places_max, 10) || 0;
const placesReserveesFresh = parseInt(evt.places_reservees, 10) || 0;
if (placesMaxFresh > 0 && placesReserveesFresh < placesMaxFresh) {
console.log('✓ Places trouvées à la lecture — bascule en mode confirmé');
isWaitlistMode = false;
evt = await runTransaction(db, async (transaction) => {
const eventSnap2 = await transaction.get(eventRef);
if (!eventSnap2.exists()) throw new Error("Événement introuvable");
const data = eventSnap2.data();
const restantes = (data.places_max || 0) - (data.places_reservees || 0);
if (restantes < nbBilletsTotaux) {
isWaitlistMode = true;
return { id: eventSnap2.id, ...data };
}
transaction.update(eventRef, {
places_reservees: (data.places_reservees || 0) + nbBilletsTotaux
});
return { id: eventSnap2.id, ...data };
});
}
} else {
evt = await runTransaction(db, async (transaction) => {
const eventRef = doc(db, "evenements", selectedEventId);
const eventSnap = await transaction.get(eventRef);
if (!eventSnap.exists()) throw new Error("Événement introuvable");
const data = eventSnap.data();
const placesRestantes = (data.places_max || 0) - (data.places_reservees || 0);
if (placesRestantes < nbBilletsTotaux) throw new Error("COMPLET");
transaction.update(eventRef, {
places_reservees: (data.places_reservees || 0) + nbBilletsTotaux
});
return { id: eventSnap.id, ...data };
});
isWaitlistMode = false;
}// Construction du tarif/billets selon le mode
let tarifChoisi, tarifPrixFormatte, tarifChoisiText, billetsDetail, billetsResume;if (isSpectacleMode && evt.tarifs) {
// MODE SPECTACLE : construire le détail multi-billets
let totalMontant = 0;
const lignes = [];
const billetsObj = {};
evt.tarifs.forEach((t, i) => {
const count = window.billetCounts[i] || 0;
if (count > 0) {
const prixUnit = parsePrix(t.prix) || 0;
totalMontant += count * prixUnit;
lignes.push(`${t.libelle} × ${count} (${count * prixUnit}€)`);
billetsObj[t.libelle] = count;
}
});
tarifChoisi = { libelle: `${nbBilletsTotaux} billet${nbBilletsTotaux > 1 ? 's' : ''}`, prix: String(totalMontant) };
tarifPrixFormatte = `${totalMontant}€`;
tarifChoisiText = lignes.join(' + ');
billetsDetail = billetsObj;
billetsResume = `${nbBilletsTotaux} place${nbBilletsTotaux > 1 ? 's' : ''} — ${totalMontant}€`;
} else {
// MODE WORKSHOP : tarif unique
const idx = parseInt(tarifChoixInput.value, 10);
tarifChoisi = (evt.tarifs && evt.tarifs[idx])
? evt.tarifs[idx]
: { libelle: 'Inscription', prix: 'Sur demande' };
tarifPrixFormatte = formatPrix(tarifChoisi.prix);
tarifChoisiText = `${tarifChoisi.libelle} — ${tarifPrixFormatte}`;
billetsDetail = null;
billetsResume = null;
}// Préparer l'objet à enregistrer
const participantData = {
evenement_id: selectedEventId,
evenement_titre: evt.titre,
prenom, nom,
email: email.toLowerCase().trim(),
telephone,
tarif_libelle: tarifChoisi.libelle,
tarif_prix: tarifPrixFormatte,
commentaire: "",
statut: isWaitlistMode ? "liste_attente" : "confirme",
paiement: "sur_place",
date_reservation: Timestamp.now()
};// En mode spectacle : ajouter le détail des billets
if (isSpectacleMode) {
participantData.type = "spectacle";
participantData.nb_places_total = nbBilletsTotaux;
participantData.billets = billetsDetail; // { "Adulte": 2, "Enfant": 1 }
participantData.billets_resume = billetsResume; // "3 places — 38€"
// Noms de chaque participant·e (1re place = réservant)
const nomsInputs = document.querySelectorAll('#accompagnantsWrap .accomp-input');
if (nomsInputs.length > 0) {
participantData.participants_noms = Array.from(nomsInputs)
.map(i => i.value.trim())
.filter(v => v);
}
}await addDoc(collection(db, "evenements_participants"), participantData);const dateStr = formatDate(evt.date_debut, evt.date_fin);// Lien Google Calendar
const calDebut = evt.date_debut?.seconds ? new Date(evt.date_debut.seconds * 1000) : new Date(evt.date_debut);
const calFin = evt.date_fin?.seconds ? new Date(evt.date_fin.seconds * 1000) : (evt.date_fin ? new Date(evt.date_fin) : new Date(calDebut));
const [ch1, cm1] = (evt.heure_debut || '09:00').split(':').map(Number);
const [ch2, cm2] = (evt.heure_fin || '18:00').split(':').map(Number);
calDebut.setHours(ch1, cm1, 0, 0);
calFin.setHours(ch2, cm2, 0, 0);
const toGcalFmt = (d) => d.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
const gcalDates = `${toGcalFmt(calDebut)}/${toGcalFmt(calFin)}`;
const gcalLocation = encodeURIComponent(evt.lieu || 'French Pole Riviera — 1 rue Oscar II, 06000 Nice');
const gcalText = encodeURIComponent(evt.titre);
const gcalDetails = encodeURIComponent((evt.description || '') + '\n\nRéservation confirmée — French Pole Riviera');
const googleCalendarUrl = `https://www.google.com/calendar/render?action=TEMPLATE&text=${gcalText}&dates=${gcalDates}&location=${gcalLocation}&details=${gcalDetails}`;const params = {
client_prenom: prenom,
client_nom: nom,
client_nom_complet: `${prenom} ${nom}`,
client_email: email,
client_telephone: telephone,
event_titre: evt.titre,
event_date: dateStr,
event_horaires: (evt.heure_debut && evt.heure_fin) ? `${evt.heure_debut} - ${evt.heure_fin}` : '',
event_lieu: evt.lieu || "French Pole Riviera — 1 rue Oscar II, 06000 Nice",
event_tarif: tarifChoisiText,
tarif_libelle: tarifChoisi.libelle,
tarif_prix: tarifPrixFormatte,
event_description: evt.description || "",
event_note: evt.note_importante || "",
google_calendar_url: googleCalendarUrl,
ics_date_debut: toGcalFmt(calDebut),
ics_date_fin: toGcalFmt(calFin),
date_reservation: new Date().toLocaleString('fr-FR'),
statut_inscription: isWaitlistMode ? "liste_attente" : "confirme",
statut_titre: isWaitlistMode ? "Inscription en liste d'attente" : "Felicitations",
statut_message: isWaitlistMode
? `Votre inscription en liste d'attente pour "${evt.titre}" est bien enregistree. Nous vous contacterons des qu'une place se libere !`
: `Felicitations, votre reservation pour "${evt.titre}" est bien enregistree. On a hate de vous retrouver au studio !`,
statut_sous_titre: isWaitlistMode ? "Vous etes sur la liste" : "Votre place est confirmee",
// 🎟️ Variables SPECTACLE (utilisées seulement si mode spectacle)
is_spectacle: isSpectacleMode,
event_billets_detail: billetsDetail,
event_billets_resume: billetsResume,
nb_places: nbBilletsTotaux
};// 📧 ENVOI EMAIL IMMÉDIAT
console.log('📧 Envoi email client inscription...', { email, isWaitlistMode });
sendEmailViaBrevo({ ...params, to_email: email })
.then(r => {
if (r.messageId) console.log('✅ Email envoyé avec succès — ID:', r.messageId);
else console.error('❌ Brevo erreur:', r);
})
.catch(err => console.error("❌ Envoi email KO:", err));// Sauvegarder pour le récap étape 4 + bouton "Ajouter à mon agenda"
window.lastReservation = {
prenom, nom, email, telephone,
titre: evt.titre,
date_debut: evt.date_debut,
date_fin: evt.date_fin,
heure_debut: evt.heure_debut,
heure_fin: evt.heure_fin,
lieu: evt.lieu || 'French Pole Riviera — 1 rue Oscar II, 06000 Nice',
description: evt.description || '',
tarif: `${tarifChoisi.libelle} : ${tarifPrixFormatte}`,
isWaitlist: isWaitlistMode
};// Sauvegarder les coordonnées pour pré-remplir la prochaine fois
try {
localStorage.setItem('fpr_client_data', JSON.stringify({
prenom, nom, email, telephone,
saved_at: Date.now()
}));
} catch (e) {
// localStorage peut être désactivé (mode privé)
}// Marquer comme traité pour éviter double-envoi
reservationProcessed = true;if (btnNext) {
btnNext.classList.remove('is-loading');
btnNext.disabled = false;
}
return true;} catch (err) {
console.error("🔴 Erreur réservation :", err);
if (err.message === "COMPLET") {
showToast("Cet événement vient de se remplir. Merci d'en choisir un autre.");
loadEvents();
closeEventModal();
} else if (err.code === "permission-denied") {
showToast("Erreur Firestore : permissions insuffisantes. Vérifiez les règles.");
} else {
showToast(`Erreur : ${err.message || 'réessayez'}`);
}
if (btnNext) {
btnNext.classList.remove('is-loading');
btnNext.disabled = false;
}
return false;
}
}document.getElementById('reservationForm').addEventListener('submit', async (e) => {
e.preventDefault();// Bloquer si on n'est pas à l'étape 4 (soumission accidentelle par Enter)
if (currentStep !== 4) {
nextStep();
return;
}// 🎯 À l'étape 4, la réservation est DÉJÀ enregistrée et le mail DÉJÀ envoyé.
// Le clic sur le bouton sert juste à fermer la modal et afficher le succès final.const reservation = window.lastReservation;
if (!reservation) {
showToast("Erreur : impossible de retrouver la réservation");
return;
}// La résa est déjà confirmée à l'étape 4 — on ferme juste la modal
closeEventModal();
loadEvents();
});function formatDate(debut, fin) {
if (!debut) return '';
const opts = { day: 'numeric', month: 'long', year: 'numeric' };
const d1 = new Date(debut.seconds ? debut.seconds * 1000 : debut);
const s1 = d1.toLocaleDateString('fr-FR', opts);
if (!fin) return s1;
const d2 = new Date(fin.seconds ? fin.seconds * 1000 : fin);
const s2 = d2.toLocaleDateString('fr-FR', opts);
if (s1 === s2) return s1;
return `Du ${s1} au ${s2}`;
}function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/[&<>"']/g, c => ({
'&':'&','<':'<','>':'>','"':'"',"'":'''
}[c]));
}// ═══════════════════════════════════════════
// AJOUT AGENDA — génération fichier .ics (iCalendar)
// ═══════════════════════════════════════════
window.downloadICS = function() {
const r = window.lastReservation;
if (!r) { showToast("Informations manquantes"); return; }// Reconstituer les dates complètes en combinant date + heure
const dateDebutObj = r.date_debut?.seconds ? new Date(r.date_debut.seconds * 1000) : new Date(r.date_debut);
const dateFinObj = r.date_fin?.seconds ? new Date(r.date_fin.seconds * 1000) : (r.date_fin ? new Date(r.date_fin) : new Date(dateDebutObj));// Appliquer les heures si présentes, sinon défaut 09:00 → 18:00
const [h1, m1] = (r.heure_debut || '09:00').split(':').map(Number);
const [h2, m2] = (r.heure_fin || '18:00').split(':').map(Number);
dateDebutObj.setHours(h1, m1, 0, 0);
dateFinObj.setHours(h2, m2, 0, 0);// Format iCalendar : YYYYMMDDTHHMMSS (UTC ou local avec TZ)
// On utilise le format "local" avec TZID pour que ça soit respecté dans Paris
const fmtDate = (d) => {
const pad = n => String(n).padStart(2, '0');
return `${d.getFullYear()}${pad(d.getMonth()+1)}${pad(d.getDate())}T${pad(d.getHours())}${pad(d.getMinutes())}00`;
};const uid = `fpr-${Date.now()}@frenchpoleriviera.com`;
const now = new Date();
const dtstamp = `${now.getUTCFullYear()}${String(now.getUTCMonth()+1).padStart(2,'0')}${String(now.getUTCDate()).padStart(2,'0')}T${String(now.getUTCHours()).padStart(2,'0')}${String(now.getUTCMinutes()).padStart(2,'0')}${String(now.getUTCSeconds()).padStart(2,'0')}Z`;// Échapper les caractères spéciaux iCal (virgule, point-virgule, retour ligne)
const icsEscape = (txt) => String(txt || '')
.replace(/\\/g, '\\\\')
.replace(/\n/g, '\\n')
.replace(/,/g, '\\,')
.replace(/;/g, '\\;');// Construire la description
const descLines = [];
if (r.description) descLines.push(r.description);
if (r.tarif) descLines.push(`Tarif : ${r.tarif}`);
descLines.push('Réservation confirmée via French Pole Riviera');
const description = descLines.join('\\n\\n');// Contenu iCalendar (standard RFC 5545)
const ics = [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//French Pole Riviera//Events//FR',
'CALSCALE:GREGORIAN',
'METHOD:PUBLISH',
'BEGIN:VTIMEZONE',
'TZID:Europe/Paris',
'BEGIN:STANDARD',
'DTSTART:19701025T030000',
'TZOFFSETFROM:+0200',
'TZOFFSETTO:+0100',
'TZNAME:CET',
'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10',
'END:STANDARD',
'BEGIN:DAYLIGHT',
'DTSTART:19700329T020000',
'TZOFFSETFROM:+0100',
'TZOFFSETTO:+0200',
'TZNAME:CEST',
'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3',
'END:DAYLIGHT',
'END:VTIMEZONE',
'BEGIN:VEVENT',
`UID:${uid}`,
`DTSTAMP:${dtstamp}`,
`DTSTART;TZID=Europe/Paris:${fmtDate(dateDebutObj)}`,
`DTEND;TZID=Europe/Paris:${fmtDate(dateFinObj)}`,
`SUMMARY:${icsEscape(r.titre)}`,
`LOCATION:${icsEscape(r.lieu)}`,
`DESCRIPTION:${icsEscape(description)}`,
'STATUS:CONFIRMED',
'BEGIN:VALARM',
'TRIGGER:-PT24H',
'ACTION:DISPLAY',
`DESCRIPTION:Rappel : ${icsEscape(r.titre)} demain`,
'END:VALARM',
'END:VEVENT',
'END:VCALENDAR'
].join('\r\n');// Téléchargement
const blob = new Blob([ics], { type: 'text/calendar;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${r.titre.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_fpr.ics`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);showToast("Événement ajouté à votre agenda ✦");
};function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 4000);
}// 🔁 Modal "Inscription déjà existante"
function showDuplicateModal(info) {
// Créer le modal s'il n'existe pas encore
let modal = document.getElementById('duplicateModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'duplicateModal';
modal.className = 'fpr-modal';
modal.innerHTML = `
`;
modal.addEventListener('click', (e) => {
if (e.target === modal) closeModal('duplicateModal');
});
document.body.appendChild(modal);
}
// Remplir le contenu
document.getElementById('duplicateModalTitle').textContent = `Bonjour ${info.prenom} 💛`;
document.getElementById('duplicateModalText').innerHTML = `
L'adresse ${info.email} est déjà inscrite à cet événement.
Statut : ${info.statut}
Inscrite le : ${info.dateInscription}
ℹ️ Une seule inscription par personne est autorisée pour chaque événement.
Vérifiez votre boîte mail (et les spams) pour retrouver votre confirmation.
`;
openModal('duplicateModal');
}window.openModal = function(id) { document.getElementById(id).classList.add('open'); };
window.closeModal = function(id) { document.getElementById(id).classList.remove('open'); };
window.handleModalClick = function(e, id) {
if (e.target === document.getElementById(id)) closeModal(id);
};// ═══════════════════════════════════════════
// PRÉ-REMPLISSAGE — si la cliente est déjà venue
// ═══════════════════════════════════════════
function restoreClientData() {
try {
const raw = localStorage.getItem('fpr_client_data');
if (!raw) return;
const data = JSON.parse(raw);// Expiration après 90 jours (RGPD friendly)
const maxAge = 90 * 24 * 60 * 60 * 1000;
if (Date.now() - (data.saved_at || 0) > maxAge) {
localStorage.removeItem('fpr_client_data');
return;
}// Pré-remplir chaque champ
if (data.prenom) document.getElementById('prenom').value = data.prenom;
if (data.nom) document.getElementById('nom').value = data.nom;
if (data.email) document.getElementById('email').value = data.email;
if (data.telephone) document.getElementById('telephone').value = data.telephone;// Notification discrète à la cliente
if (data.prenom) {
setTimeout(() => {
showToast(`✦ Bon retour parmi nous, ${data.prenom} !`);
}, 800);
}
} catch (e) {
// localStorage désactivé ou JSON corrompu → on ignore
}
}
restoreClientData();// ═══════════════════════════════════════════════════
// 👤 ESPACE PERSONNEL — consulter / annuler ses réservations
// ═══════════════════════════════════════════════════
const ESP = { email: '', code: '', reservations: [] };async function espEnvoiBrevo(toEmail, toNom, sujet, texte) {
const html = `${texte.replace(/\n/g,' ')}
`;
const payload = {
sender: { name: BREVO_CONFIG.SENDER_NAME, email: BREVO_CONFIG.SENDER_EMAIL },
to: [{ email: toEmail, name: toNom || toEmail }],
bcc: [{ email: BREVO_CONFIG.ADMIN_EMAIL, name: "French Pole Riviera" }],
subject: sujet,
htmlContent: html,
textContent: texte,
replyTo: { email: BREVO_CONFIG.ADMIN_EMAIL, name: "Juliette - French Pole Riviera" },
headers: { 'X-Mailin-custom': 'transactional-espace-perso', 'X-Entity-Ref-ID': `fpr-esp-${Date.now()}` }
};
return fetch('https://api.brevo.com/v3/smtp/email', {
method: 'POST',
headers: { 'accept': 'application/json', 'content-type': 'application/json', 'api-key': BREVO_CONFIG.API_KEY },
body: JSON.stringify(payload)
}).then(r => r.json());
}function espGetModal() {
let m = document.getElementById('espModal');
if (!m) {
m = document.createElement('div');
m.id = 'espModal';
m.className = 'fpr-modal';
m.onclick = (e) => { if (e.target === m) closeEspacePerso(); };
document.body.appendChild(m);
}
return m;
}
window.closeEspacePerso = function() {
const m = document.getElementById('espModal');
if (m) m.classList.remove('open');
};window.openEspacePerso = function() {
const m = espGetModal();
m.innerHTML = ``;
m.classList.add('open');
setTimeout(() => document.getElementById('espEmail')?.focus(), 100);
};window.espEnvoyerCode = async function() {
const email = (document.getElementById('espEmail')?.value || '').trim().toLowerCase();
const msg = document.getElementById('espMsg');
const btn = document.getElementById('espSendBtn');
if (!email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
msg.style.color = 'var(--danger, #D48882)';
msg.textContent = "Merci d'entrer un email valide.";
return;
}
btn.disabled = true; btn.textContent = "Envoi en cours…";
ESP.email = email;
ESP.code = String(Math.floor(100000 + Math.random() * 900000));
const texte = `Bonjour,\n\nVoici votre code de vérification pour accéder à vos réservations French Pole Riviera :\n\n${ESP.code}\n\nCe code est valable pour cette session uniquement.\nSi vous n'êtes pas à l'origine de cette demande, ignorez cet email.\n\nFrench Pole Riviera`;
try {
await espEnvoiBrevo(email, '', `Votre code de vérification : ${ESP.code}`, texte);
espEtapeCode();
} catch (e) {
console.error(e);
msg.style.color = 'var(--danger, #D48882)';
msg.textContent = "Erreur d'envoi. Réessayez dans un instant.";
btn.disabled = false; btn.textContent = "Recevoir mon code";
}
};function espEtapeCode() {
const m = espGetModal();
m.innerHTML = `
✕ Vérification Un code à 6 chiffres a été envoyé à ${espEsc(ESP.email)} . Saisissez-le ci-dessous (pensez à vérifier vos spams).
Voir mes réservations
← Changer d'email
`;
setTimeout(() => document.getElementById('espCode')?.focus(), 100);
}window.espVerifierCode = async function() {
const saisi = (document.getElementById('espCode')?.value || '').trim();
const msg = document.getElementById('espMsg');
if (saisi !== ESP.code) {
msg.style.color = 'var(--danger, #D48882)';
msg.textContent = "Code incorrect. Vérifiez et réessayez.";
return;
}
msg.style.color = 'var(--text-3)';
msg.textContent = "Chargement…";
try {
const q = query(collection(db, "evenements_participants"), where("email", "==", ESP.email));
const snap = await getDocs(q);
ESP.reservations = [];
snap.forEach(d => ESP.reservations.push({ id: d.id, ...d.data() }));
espAfficherReservations();
} catch (e) {
console.error(e);
msg.style.color = 'var(--danger, #D48882)';
msg.textContent = "Erreur de chargement. Réessayez.";
}
};function espAfficherReservations() {
const m = espGetModal();
let liste = '';
if (ESP.reservations.length === 0) {
liste = `Aucune réservation trouvée pour cet email.
Si vous venez de réserver, vérifiez l'email saisi. Pour participer à un événement, il suffit de le réserver depuis la liste — votre espace se remplira automatiquement.
Voir les événements `;
} else {
ESP.reservations.forEach(r => {
const statutBadge = r.statut === 'liste_attente'
? `Liste d'attente `
: `Confirmée `;
const placesInfo = (r.type === 'spectacle' && r.nb_places_total) ? ` • ${r.nb_places_total} place${r.nb_places_total>1?'s':''}` : '';
liste += `
${espEsc(r.evenement_titre || 'Événement')}
${statutBadge}
🎟️ ${espEsc(r.tarif_libelle || '')}${r.tarif_prix ? ' — ' + espEsc(r.tarif_prix) : ''}${placesInfo}
Annuler cette réservation `;
});
}
m.innerHTML = `
✕ Mes réservations ${espEsc(ESP.email)}
${liste}
Pour toute modification, contactez-nous directement.
`;
}function espEsc(s) {
return String(s == null ? '' : s).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c]));
}window.espAnnuler = async function(reservationId) {
const r = ESP.reservations.find(x => x.id === reservationId);
if (!r) return;
if (!confirm(`Annuler votre réservation pour "${r.evenement_titre}" ?\n\nCette action est définitive.`)) return;const placesALiberer = (r.type === 'spectacle' && r.nb_places_total) ? r.nb_places_total : 1;
const etaitConfirme = (r.statut || 'confirme') === 'confirme';try {
await deleteDoc(doc(db, "evenements_participants", reservationId));
if (etaitConfirme && r.evenement_id) {
const evtRef = doc(db, "evenements", r.evenement_id);
await runTransaction(db, async (t) => {
const snap = await t.get(evtRef);
if (!snap.exists()) return;
const actuel = snap.data().places_reservees || 0;
t.update(evtRef, { places_reservees: Math.max(0, actuel - placesALiberer) });
});
}
const texte = `Bonjour ${r.prenom || ''},\n\nVotre réservation a bien été annulée :\n\n• Événement : ${r.evenement_titre}\n• Tarif : ${r.tarif_libelle || ''}${r.tarif_prix ? ' — ' + r.tarif_prix : ''}${placesALiberer > 1 ? '\n• Places : ' + placesALiberer : ''}\n\nAu plaisir de vous revoir bientôt.\n\nFrench Pole Riviera`;
espEnvoiBrevo(r.email, r.prenom, `Annulation confirmée — ${r.evenement_titre}`, texte).catch(e => console.error(e));
ESP.reservations = ESP.reservations.filter(x => x.id !== reservationId);
showToast("Réservation annulée ✦");
espAfficherReservations();
loadEvents();
} catch (e) {
console.error(e);
showToast("Erreur lors de l'annulation", 'error');
}
};loadEvents();