/** * Gestion du formulaire de contact en modal avec envoi AJAX. * Protections : CSRF, honeypot, limitation de débit côté session. */ class ContactForm { /** * @param {HTMLFormElement} form * @param {HTMLElement} modal * @param {string} url - endpoint POST * @param {string} csrfToken */ constructor(form, modal, url, csrfToken) { this.form = form; this.modal = modal; this.url = url; this.csrfToken = csrfToken; this.overlay = modal.querySelector('#contact-overlay'); this.btnClose = modal.querySelector('#contact-modal-close'); this.btnOpen = document.querySelector('#open-contact-modal'); this.submit = form.querySelector('#contact-submit'); this.success = modal.querySelector('#contact-success'); this.globalErr = modal.querySelector('#contact-global-error'); this._bindEvents(); } // ── Initialisation ───────────────────────────────────────────────────────── _bindEvents() { // Ouvrir if (this.btnOpen) { this.btnOpen.addEventListener('click', () => { this._setPropertyId(this.btnOpen.dataset.property); this.open(); }); } // Fermer if (this.btnClose) this.btnClose.addEventListener('click', () => this.close()); if (this.overlay) this.overlay.addEventListener('click', () => this.close()); // Clavier document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !this.modal.hidden) this.close(); }); // Soumission this.form.addEventListener('submit', (e) => this._handleSubmit(e)); // Nettoyage des erreurs à la saisie this.form.querySelectorAll('input, textarea').forEach((el) => { el.addEventListener('input', () => this._clearFieldError(el.name)); }); } // ── API publique ─────────────────────────────────────────────────────────── open() { this._resetForm(); this.modal.hidden = false; document.body.style.overflow = 'hidden'; this.form.querySelector('#id_name')?.focus(); } close() { this.modal.hidden = true; document.body.style.overflow = ''; } // ── Soumission ───────────────────────────────────────────────────────────── async _handleSubmit(e) { e.preventDefault(); if (!this._validateLocal()) return; this._setLoading(true); this._hideGlobalError(); const formData = new FormData(this.form); try { const response = await fetch(this.url, { method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-CSRFToken': this.csrfToken, }, }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); if (data.success) { this._showSuccess(); } else if (data.errors) { this._showFieldErrors(data.errors); } else if (data.error) { this._showGlobalError(data.error); } } catch (err) { this._showGlobalError( (window.I18N?.networkError) || 'Une erreur est survenue. Veuillez réessayer.' ); } finally { this._setLoading(false); } } // ── Validation locale légère ─────────────────────────────────────────────── _validateLocal() { let valid = true; const name = this.form.querySelector('#id_name'); const email = this.form.querySelector('#id_email'); const msg = this.form.querySelector('#id_message'); if (!name.value.trim()) { this._setFieldError('name', name.required ? 'Ce champ est requis.' : ''); valid = false; } if (!email.value.trim() || !email.value.includes('@')) { this._setFieldError('email', 'Adresse email invalide.'); valid = false; } if (!msg.value.trim() || msg.value.trim().length < 10) { this._setFieldError('message', 'Votre message est trop court.'); valid = false; } return valid; } // ── Utilitaires ──────────────────────────────────────────────────────────── _setPropertyId(id) { const input = this.form.querySelector('#id_property_id'); if (input) input.value = id || ''; } _setLoading(loading) { if (!this.submit) return; const i18n = window.I18N || {}; this.submit.disabled = loading; this.submit.textContent = loading ? (i18n.sending || 'Envoi en cours…') : (i18n.send || 'Envoyer'); } _showSuccess() { this.form.hidden = true; this.success.hidden = false; } _resetForm() { this.form.reset(); this.form.hidden = false; this.success.hidden = true; this._hideGlobalError(); this.form.querySelectorAll('.field-error').forEach(el => { el.textContent = ''; }); } _setFieldError(name, msg) { const el = this.form.querySelector(`.field-error[data-field="${name}"]`); if (el) el.textContent = msg; } _clearFieldError(name) { this._setFieldError(name, ''); } _showFieldErrors(errors) { for (const [field, msgs] of Object.entries(errors)) { const list = Array.isArray(msgs) ? msgs : [msgs]; this._setFieldError(field, list.join(' ')); } } _showGlobalError(msg) { if (!this.globalErr) return; this.globalErr.textContent = msg; this.globalErr.hidden = false; } _hideGlobalError() { if (this.globalErr) this.globalErr.hidden = true; } }