From a4e0cb71e98d6547cd570ea0c56f402b9db9d0f0 Mon Sep 17 00:00:00 2001 From: skycel Date: Wed, 4 Feb 2026 21:10:40 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Story=205.3:=20persistance=20localS?= =?UTF-8?q?torage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/js/contact-form.js | 99 ++++++++++++++++++-- assets/js/state.js | 57 +++++++++++ docs/stories/5.3.persistance-localstorage.md | 71 +++++++------- pages/contact.php | 3 +- tests/contact-state.test.php | 31 ++++++ tests/contact.test.php | 2 + tests/run.ps1 | 1 + 7 files changed, 222 insertions(+), 42 deletions(-) create mode 100644 assets/js/state.js create mode 100644 tests/contact-state.test.php diff --git a/assets/js/contact-form.js b/assets/js/contact-form.js index 22a168d..8d2071c 100644 --- a/assets/js/contact-form.js +++ b/assets/js/contact-form.js @@ -1,6 +1,6 @@ -/** +/** * Validation du formulaire de contact - * JavaScript vanilla - pas de dépendances + * JavaScript vanilla - pas de dĂ©pendances */ class FormValidator { @@ -21,13 +21,13 @@ class FormValidator { required: true, minLength: 2, maxLength: 100, - message: 'Veuillez entrer votre nom (2 caractčres minimum)' + 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)' + message: 'Veuillez entrer votre prĂ©nom (2 caractères minimum)' }, email: { required: true, @@ -36,19 +36,19 @@ class FormValidator { }, categorie: { required: true, - message: 'Veuillez sélectionner une catégorie' + message: 'Veuillez sĂ©lectionner une catĂ©gorie' }, objet: { required: true, minLength: 5, maxLength: 200, - message: 'Veuillez entrer un objet (5 caractčres minimum)' + 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)' + message: 'Veuillez entrer votre message (20 caractères minimum)' } }; @@ -98,7 +98,7 @@ class FormValidator { if (isValid && rule.maxLength && value.length > rule.maxLength) { isValid = false; - errorMessage = `Maximum ${rule.maxLength} caractčres`; + errorMessage = `Maximum ${rule.maxLength} caractères`; } if (isValid && rule.email && value) { @@ -212,6 +212,89 @@ class FormValidator { } } +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 = ''; + }); + } +} + document.addEventListener('DOMContentLoaded', () => { window.contactFormValidator = new FormValidator('contact-form'); + window.contactFormPersistence = new ContactFormPersistence('contact-form'); }); diff --git a/assets/js/state.js b/assets/js/state.js new file mode 100644 index 0000000..0317b20 --- /dev/null +++ b/assets/js/state.js @@ -0,0 +1,57 @@ +/** + * Gestionnaire d'état pour le localStorage + */ +const AppState = { + STORAGE_KEY: 'portfolio_contact_form', + EXCLUDED_FIELDS: ['csrf_token', 'password', 'recaptcha_token'], + + isStorageAvailable() { + try { + const test = '__storage_test__'; + localStorage.setItem(test, test); + localStorage.removeItem(test); + return true; + } catch (e) { + return false; + } + }, + + saveFormData(data) { + if (!this.isStorageAvailable()) return; + + try { + const filteredData = {}; + Object.keys(data).forEach((key) => { + if (!this.EXCLUDED_FIELDS.includes(key)) { + filteredData[key] = data[key]; + } + }); + + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(filteredData)); + } catch (e) { + console.warn('Impossible de sauvegarder dans localStorage:', e); + } + }, + + getFormData() { + if (!this.isStorageAvailable()) return null; + + try { + const data = localStorage.getItem(this.STORAGE_KEY); + return data ? JSON.parse(data) : null; + } catch (e) { + console.warn('Impossible de charger depuis localStorage:', e); + return null; + } + }, + + clearFormData() { + if (!this.isStorageAvailable()) return; + + try { + localStorage.removeItem(this.STORAGE_KEY); + } catch (e) { + // Silencieux + } + } +}; diff --git a/docs/stories/5.3.persistance-localstorage.md b/docs/stories/5.3.persistance-localstorage.md index 63e097f..a6b4a32 100644 --- a/docs/stories/5.3.persistance-localstorage.md +++ b/docs/stories/5.3.persistance-localstorage.md @@ -2,7 +2,7 @@ ## Status -Ready for Dev +review ## Story @@ -21,34 +21,34 @@ Ready for Dev ## Tasks / Subtasks -- [] **Task 1 : CrĂ©er le gestionnaire de stockage** (AC: 6) - - [] ClĂ© unique `portfolio_contact_form` - - [] MĂ©thodes save, load, clear - - [] Gestion des erreurs (localStorage indisponible) +- [x] **Task 1 : CrĂ©er le gestionnaire de stockage** (AC: 6) + - [x] ClĂ© unique `portfolio_contact_form` + - [x] MĂ©thodes save, load, clear + - [x] Gestion des erreurs (localStorage indisponible) -- [] **Task 2 : Sauvegarder automatiquement** (AC: 1) - - [] Écouter l'Ă©vĂ©nement `input` sur chaque champ - - [] Debounce pour Ă©viter trop d'Ă©critures (500ms) - - [] Sauvegarder l'Ă©tat complet +- [x] **Task 2 : Sauvegarder automatiquement** (AC: 1) + - [x] Écouter l'Ă©vĂ©nement `input` sur chaque champ + - [x] Debounce pour Ă©viter trop d'Ă©critures (500ms) + - [x] Sauvegarder l'Ă©tat complet -- [] **Task 3 : Restaurer au chargement** (AC: 2) - - [] Charger les donnĂ©es au DOMContentLoaded - - [] PrĂ©-remplir chaque champ - - [] Mettre Ă  jour le compteur de caractères +- [x] **Task 3 : Restaurer au chargement** (AC: 2) + - [x] Charger les donnĂ©es au DOMContentLoaded + - [x] PrĂ©-remplir chaque champ + - [x] Mettre Ă  jour le compteur de caractères -- [] **Task 4 : Vider après envoi rĂ©ussi** (AC: 3) - - [] Appeler clear() après succès (Ă©vĂ©nement formSuccess) - - [] RĂ©initialiser le formulaire +- [x] **Task 4 : Vider après envoi rĂ©ussi** (AC: 3) + - [x] Appeler clear() après succès (Ă©vĂ©nement formSuccess) + - [x] RĂ©initialiser le formulaire -- [] **Task 5 : Bouton "Effacer"** (AC: 4) - - [] Écouter le clic sur le bouton (id="clear-form-btn") - - [] Vider le localStorage - - [] RĂ©initialiser le formulaire - - [] Confirmation avec confirm() +- [x] **Task 5 : Bouton "Effacer"** (AC: 4) + - [x] Écouter le clic sur le bouton (id="clear-form-btn") + - [x] Vider le localStorage + - [x] RĂ©initialiser le formulaire + - [x] Confirmation avec confirm() -- [] **Task 6 : Exclure les donnĂ©es sensibles** (AC: 5) - - [] Ne pas stocker csrf_token, password, recaptcha_token - - [] DocumentĂ© dans EXCLUDED_FIELDS +- [x] **Task 6 : Exclure les donnĂ©es sensibles** (AC: 5) + - [x] Ne pas stocker csrf_token, password, recaptcha_token + - [x] DocumentĂ© dans EXCLUDED_FIELDS ## Dev Notes @@ -270,7 +270,11 @@ Exemple de donnĂ©es stockĂ©es: ## 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 Ă  6 dans l’ordre avec tests Ă  chaque Ă©tape. +- IntĂ©grer le stockage dans `contact-form.js` et scripts dans la page. ### File List | File | Action | Description | @@ -278,16 +282,16 @@ Claude Opus 4.5 (claude-opus-4-5-20251101) | `assets/js/state.js` | Created | Objet AppState pour gestion localStorage | | `assets/js/contact-form.js` | Modified | Ajout classe ContactFormPersistence | | `pages/contact.php` | Modified | Bouton Effacer + script state.js | +| `tests/contact-state.test.php` | Added | Tests persistance localStorage | +| `tests/contact.test.php` | Modified | VĂ©rification clear button + scripts | +| `tests/run.ps1` | Modified | Ajout du test contact-state | ### Completion Notes -- AppState avec clĂ© unique `portfolio_contact_form` -- VĂ©rification disponibilitĂ© localStorage (try/catch) -- Filtrage des champs sensibles (csrf_token, password, recaptcha_token) -- Debounce de 500ms sur la sauvegarde -- Restauration automatique au chargement de la page -- Bouton "Effacer" avec confirmation et reset complet -- ÉvĂ©nement `formSuccess` pour vider après envoi rĂ©ussi -- Scripts chargĂ©s avec defer pour ne pas bloquer le rendu +- AppState avec clĂ© unique `portfolio_contact_form` et exclusions sensibles +- Sauvegarde avec debounce 500ms + restauration au chargement +- Bouton Effacer avec confirm + reset complet +- Listener `formSuccess` pour purge post-envoi +- Tests : `powershell -ExecutionPolicy Bypass -File tests/run.ps1` ### Debug Log References Aucun problème rencontrĂ©. @@ -298,3 +302,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 | Persistance localStorage | Amelia (Dev) | diff --git a/pages/contact.php b/pages/contact.php index 88de4c7..6a77d27 100644 --- a/pages/contact.php +++ b/pages/contact.php @@ -136,7 +136,7 @@ include_template('navbar', compact('currentPage')); - @@ -146,6 +146,7 @@ include_template('navbar', compact('currentPage')); + diff --git a/tests/contact-state.test.php b/tests/contact-state.test.php new file mode 100644 index 0000000..f04d575 --- /dev/null +++ b/tests/contact-state.test.php @@ -0,0 +1,31 @@ +]*required/', $content) === 1, 'nom missing required'); diff --git a/tests/run.ps1 b/tests/run.ps1 index c3ca832..c6ba8b7 100644 --- a/tests/run.ps1 +++ b/tests/run.ps1 @@ -22,4 +22,5 @@ php (Join-Path $here 'passions.test.php') php (Join-Path $here 'testimonials.test.php') php (Join-Path $here 'contact.test.php') php (Join-Path $here 'contact-validation.test.php') +php (Join-Path $here 'contact-state.test.php') 'OK'