diff --git a/assets/js/contact-form.js b/assets/js/contact-form.js new file mode 100644 index 0000000..22a168d --- /dev/null +++ b/assets/js/contact-form.js @@ -0,0 +1,217 @@ +/** + * 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; + } +} + +document.addEventListener('DOMContentLoaded', () => { + window.contactFormValidator = new FormValidator('contact-form'); +}); diff --git a/docs/stories/5.2.validation-javascript.md b/docs/stories/5.2.validation-javascript.md index ee0d5af..abf14f3 100644 --- a/docs/stories/5.2.validation-javascript.md +++ b/docs/stories/5.2.validation-javascript.md @@ -2,7 +2,7 @@ ## Status -Ready for Dev +review ## Story @@ -21,29 +21,29 @@ Ready for Dev ## Tasks / Subtasks -- [] **Task 1 : CrĂ©er le validateur de formulaire** (AC: 6) - - [] CrĂ©er `assets/js/contact-form.js` - - [] Classe ou objet `FormValidator` - - [] MĂ©thodes de validation par type de champ +- [x] **Task 1 : CrĂ©er le validateur de formulaire** (AC: 6) + - [x] CrĂ©er `assets/js/contact-form.js` + - [x] Classe ou objet `FormValidator` + - [x] MĂ©thodes de validation par type de champ -- [] **Task 2 : ImplĂ©menter la validation au blur** (AC: 1) - - [] Écouter l'Ă©vĂ©nement `blur` sur chaque champ - - [] Valider le champ concernĂ© - - [] Afficher/masquer l'erreur +- [x] **Task 2 : ImplĂ©menter la validation au blur** (AC: 1) + - [x] Écouter l'Ă©vĂ©nement `blur` sur chaque champ + - [x] Valider le champ concernĂ© + - [x] Afficher/masquer l'erreur -- [] **Task 3 : ImplĂ©menter la validation Ă la soumission** (AC: 1) - - [] Écouter l'Ă©vĂ©nement `submit` - - [] Valider tous les champs - - [] EmpĂŞcher l'envoi si erreurs +- [x] **Task 3 : ImplĂ©menter la validation Ă la soumission** (AC: 1) + - [x] Écouter l'Ă©vĂ©nement `submit` + - [x] Valider tous les champs + - [x] EmpĂŞcher l'envoi si erreurs -- [] **Task 4 : Afficher les erreurs** (AC: 2, 3, 4) - - [] Message sous le champ (data-error) - - [] Bordure rouge sur le champ (classes Tailwind) - - [] Messages clairs et actionnables +- [x] **Task 4 : Afficher les erreurs** (AC: 2, 3, 4) + - [x] Message sous le champ (data-error) + - [x] Bordure rouge sur le champ (classes Tailwind) + - [x] Messages clairs et actionnables -- [] **Task 5 : GĂ©rer l'Ă©tat du bouton** (AC: 5) - - [] DĂ©sactiver si erreurs - - [] RĂ©activer quand tout est valide +- [x] **Task 5 : GĂ©rer l'Ă©tat du bouton** (AC: 5) + - [x] DĂ©sactiver si erreurs + - [x] RĂ©activer quand tout est valide ## Dev Notes @@ -320,25 +320,28 @@ document.addEventListener('DOMContentLoaded', () => { ## Dev Agent Record ### Agent Model Used -Claude Opus 4.5 (claude-opus-4-5-20251101) +GPT-5 Codex + +### Implementation Plan +- ImplĂ©menter les tâches 1 Ă 5 dans l’ordre avec tests Ă chaque Ă©tape. +- Mettre Ă jour le formulaire pour les hooks JS (data-error, id submit). ### File List | File | Action | Description | |------|--------|-------------| | `assets/js/contact-form.js` | Created | Classe FormValidator avec validation complète | -| `pages/contact.php` | Modified | Lien vers le script JS, classes Tailwind pour erreurs | +| `pages/contact.php` | Modified | Hooks JS (data-error, submit-btn, script) | +| `tests/contact-validation.test.php` | Added | Tests du validateur JS (prĂ©sence/mĂ©thodes) | +| `tests/contact.test.php` | Modified | VĂ©rifications markup contact + data-error | +| `tests/run.ps1` | Modified | Ajout du test contact-validation | ### Completion Notes -- Classe FormValidator en JavaScript vanilla (pas de dĂ©pendances) -- Validation au blur et Ă la soumission -- Messages d'erreur sous chaque champ avec data-error -- Bordure rouge sur les champs invalides (classes Tailwind) -- Bouton submit dĂ©sactivĂ© si erreurs (updateSubmitButton) -- Compteur de caractères en temps rĂ©el -- Focus automatique sur le premier champ en erreur -- Validation email avec regex -- ÉvĂ©nement 'validSubmit' dispatchĂ© quand tout est valide -- Gestion du reset du formulaire +- Task 1 : classe FormValidator et règles de validation mises en place (JS vanilla) +- Task 2 : validation au blur + gestion des erreurs champ par champ +- Task 3 : validation Ă la soumission et blocage si erreurs +- Task 4 : messages d'erreur + bordures invalides configurĂ©s +- Task 5 : dĂ©sactivation/rĂ©activation du bouton d'envoi +- Tests : `powershell -ExecutionPolicy Bypass -File tests/run.ps1` ### Debug Log References Aucun problème rencontrĂ©. @@ -349,3 +352,4 @@ Aucun problème rencontrĂ©. |------|---------|-------------|--------| | 2026-01-22 | 0.1 | CrĂ©ation initiale | Sarah (PO) | | 2026-01-24 | 1.0 | ImplĂ©mentation complète | James (Dev) | +| 2026-02-04 | 1.1 | Validation JS cĂ´tĂ© client | Amelia (Dev) | diff --git a/pages/contact.php b/pages/contact.php index d37b547..88de4c7 100644 --- a/pages/contact.php +++ b/pages/contact.php @@ -41,6 +41,7 @@ include_template('navbar', compact('currentPage')); autocomplete="family-name" placeholder="Dupont" > +
0 / 5000 caractères