/** * Validation du formulaire de contact * JavaScript vanilla - pas de dépendances */ class FormValidator { constructor(formId) { this.form = document.getElementById(formId); if (!this.form) return; this.submitBtn = document.getElementById('submit-btn'); this.fields = {}; this.errors = {}; this.init(); } init() { this.rules = { nom: { required: true, minLength: 2, maxLength: 100, message: 'Veuillez entrer votre nom (2 caractères minimum)' }, prenom: { required: true, minLength: 2, maxLength: 100, message: 'Veuillez entrer votre prénom (2 caractères minimum)' }, email: { required: true, email: true, message: 'Veuillez entrer une adresse email valide' }, categorie: { required: true, message: 'Veuillez sélectionner une catégorie' }, objet: { required: true, minLength: 5, maxLength: 200, message: 'Veuillez entrer un objet (5 caractères minimum)' }, message: { required: true, minLength: 20, maxLength: 5000, message: 'Veuillez entrer votre message (20 caractères minimum)' } }; Object.keys(this.rules).forEach((fieldName) => { this.fields[fieldName] = this.form.querySelector(`[name="${fieldName}"]`); }); this.bindEvents(); } bindEvents() { Object.keys(this.fields).forEach((fieldName) => { const field = this.fields[fieldName]; if (field) { field.addEventListener('blur', () => this.validateField(fieldName)); field.addEventListener('input', () => this.clearError(fieldName)); } }); this.form.addEventListener('submit', (e) => this.handleSubmit(e)); const messageField = this.fields.message; if (messageField) { messageField.addEventListener('input', () => this.updateCharCount()); } } validateField(fieldName) { const field = this.fields[fieldName]; const rule = this.rules[fieldName]; if (!field || !rule) return true; const value = field.value.trim(); let isValid = true; let errorMessage = ''; if (rule.required && !value) { isValid = false; errorMessage = rule.message; } if (isValid && rule.minLength && value.length < rule.minLength) { isValid = false; errorMessage = rule.message; } if (isValid && rule.maxLength && value.length > rule.maxLength) { isValid = false; errorMessage = `Maximum ${rule.maxLength} caractères`; } if (isValid && rule.email && value) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { isValid = false; errorMessage = rule.message; } } if (isValid) { this.clearError(fieldName); } else { this.showError(fieldName, errorMessage); } this.errors[fieldName] = !isValid; this.updateSubmitButton(); return isValid; } validateAll() { let allValid = true; Object.keys(this.rules).forEach((fieldName) => { if (!this.validateField(fieldName)) { allValid = false; } }); return allValid; } showError(fieldName, message) { const field = this.fields[fieldName]; const errorEl = this.form.querySelector(`[data-error="${fieldName}"]`); if (field) { field.classList.add('input-error'); field.setAttribute('aria-invalid', 'true'); } if (errorEl) { errorEl.textContent = message; errorEl.classList.remove('hidden'); } } clearError(fieldName) { const field = this.fields[fieldName]; const errorEl = this.form.querySelector(`[data-error="${fieldName}"]`); if (field) { field.classList.remove('input-error'); field.removeAttribute('aria-invalid'); } if (errorEl) { errorEl.textContent = ''; errorEl.classList.add('hidden'); } this.errors[fieldName] = false; this.updateSubmitButton(); } updateSubmitButton() { const hasErrors = Object.values(this.errors).some((err) => err); if (this.submitBtn) { this.submitBtn.disabled = hasErrors; } } updateCharCount() { const messageField = this.fields.message; const countEl = document.getElementById('message-count'); if (messageField && countEl) { countEl.textContent = messageField.value.length; } } handleSubmit(e) { e.preventDefault(); if (!this.validateAll()) { const firstError = Object.keys(this.errors).find((key) => this.errors[key]); if (firstError && this.fields[firstError]) { this.fields[firstError].focus(); } return; } this.form.dispatchEvent(new CustomEvent('validSubmit')); } getFormData() { const formData = {}; Object.keys(this.fields).forEach((fieldName) => { if (this.fields[fieldName]) { formData[fieldName] = this.fields[fieldName].value.trim(); } }); const entreprise = this.form.querySelector('[name="entreprise"]'); if (entreprise) { formData.entreprise = entreprise.value.trim(); } return formData; } } class ContactFormPersistence { constructor(formId) { this.form = document.getElementById(formId); if (!this.form) return; this.debounceTimer = null; this.init(); } init() { this.loadSavedData(); this.bindEvents(); } bindEvents() { this.form.addEventListener('input', () => { clearTimeout(this.debounceTimer); this.debounceTimer = setTimeout(() => this.saveData(), 500); }); const clearBtn = document.getElementById('clear-form-btn'); if (clearBtn) { clearBtn.addEventListener('click', () => this.clearForm()); } this.form.addEventListener('formSuccess', () => { AppState.clearFormData(); }); } saveData() { const formData = new FormData(this.form); const data = {}; formData.forEach((value, key) => { data[key] = value; }); AppState.saveFormData(data); } loadSavedData() { const savedData = AppState.getFormData(); if (!savedData) return; Object.keys(savedData).forEach((key) => { const field = this.form.querySelector(`[name="${key}"]`); if (field && savedData[key]) { field.value = savedData[key]; } }); const messageField = this.form.querySelector('[name="message"]'); const countEl = document.getElementById('message-count'); if (messageField && countEl) { countEl.textContent = messageField.value.length; } } clearForm() { if (!confirm('Êtes-vous sûr de vouloir effacer le formulaire ?')) { return; } AppState.clearFormData(); this.form.reset(); const countEl = document.getElementById('message-count'); if (countEl) { countEl.textContent = '0'; } this.form.querySelectorAll('.input-error').forEach((el) => { el.classList.remove('input-error'); }); this.form.querySelectorAll('[data-error]').forEach((el) => { el.classList.add('hidden'); el.textContent = ''; }); } } const RecaptchaService = { siteKey: null, init() { this.siteKey = window.RECAPTCHA_SITE_KEY || null; }, isAvailable() { return this.siteKey && typeof grecaptcha !== 'undefined'; }, async getToken(action = 'contact') { if (!this.isAvailable()) { console.warn('reCAPTCHA non disponible, envoi sans protection'); return ''; } return new Promise((resolve) => { grecaptcha.ready(() => { grecaptcha.execute(this.siteKey, { action }) .then((token) => resolve(token)) .catch((error) => { console.error('Erreur reCAPTCHA:', error); resolve(''); }); }); }); } }; class ContactFormSubmit { constructor(formId) { this.form = document.getElementById(formId); if (!this.form) return; this.submitBtn = document.getElementById('submit-btn'); this.submitText = document.getElementById('submit-text'); this.submitLoading = document.getElementById('submit-loading'); this.successMessage = document.getElementById('success-message'); this.errorMessage = document.getElementById('error-message'); this.errorText = document.getElementById('error-text'); this.isSubmitting = false; this.init(); } init() { this.form.addEventListener('validSubmit', () => this.handleSubmit()); } async handleSubmit() { if (this.isSubmitting) return; this.setLoadingState(true); this.hideMessages(); try { const formData = window.contactFormValidator.getFormData(); const csrfInput = this.form.querySelector('[name="csrf_token"]'); if (csrfInput) { formData.csrf_token = csrfInput.value; } formData.recaptcha_token = await RecaptchaService.getToken('contact'); const response = await fetch('/api/contact.php', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(formData) }); const result = await response.json(); if (result.success) { this.handleSuccess(result.message); } else { this.handleError(result.error || 'Une erreur est survenue'); } } catch (error) { console.error('Erreur envoi formulaire:', error); this.handleError('Impossible de contacter le serveur. Vérifiez votre connexion.'); } finally { this.setLoadingState(false); } } setLoadingState(loading) { this.isSubmitting = loading; if (this.submitBtn) { this.submitBtn.disabled = loading; } if (this.submitText && this.submitLoading) { if (loading) { this.submitText.classList.add('hidden'); this.submitLoading.classList.remove('hidden'); } else { this.submitText.classList.remove('hidden'); this.submitLoading.classList.add('hidden'); } } } handleSuccess(message) { this.form.classList.add('hidden'); if (this.successMessage) { this.successMessage.classList.remove('hidden'); this.successMessage.scrollIntoView({ behavior: 'smooth', block: 'center' }); } AppState.clearFormData(); this.form.reset(); this.form.dispatchEvent(new CustomEvent('formSuccess')); } handleError(message) { if (this.errorMessage && this.errorText) { this.errorText.textContent = message; this.errorMessage.classList.remove('hidden'); this.errorMessage.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } hideMessages() { if (this.successMessage) { this.successMessage.classList.add('hidden'); } if (this.errorMessage) { this.errorMessage.classList.add('hidden'); } } } document.addEventListener('DOMContentLoaded', () => { RecaptchaService.init(); window.contactFormValidator = new FormValidator('contact-form'); window.contactFormPersistence = new ContactFormPersistence('contact-form'); window.contactFormSubmit = new ContactFormSubmit('contact-form'); });