306 lines
8.7 KiB
Markdown
306 lines
8.7 KiB
Markdown
# Story 5.3: Persistance des Données avec localStorage
|
||
|
||
## Status
|
||
|
||
review
|
||
|
||
## Story
|
||
|
||
**As a** visiteur,
|
||
**I want** que mes données soient sauvegardées si je quitte la page,
|
||
**so that** je ne ressaisisse pas tout si je reviens plus tard.
|
||
|
||
## Acceptance Criteria
|
||
|
||
1. Chaque modification d'un champ sauvegarde automatiquement dans localStorage
|
||
2. Au chargement de la page, les champs sont pré-remplis avec les données sauvegardées
|
||
3. Le localStorage est vidé après un envoi réussi du formulaire
|
||
4. Un bouton "Effacer le formulaire" permet de réinitialiser manuellement
|
||
5. Les données sensibles (si ajoutées plus tard) ne sont PAS stockées
|
||
6. Le stockage utilise une clé unique (ex: `portfolio_contact_form`)
|
||
|
||
## Tasks / Subtasks
|
||
|
||
- [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)
|
||
|
||
- [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
|
||
|
||
- [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
|
||
|
||
- [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
|
||
|
||
- [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()
|
||
|
||
- [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
|
||
|
||
### Gestionnaire de Stockage (state.js)
|
||
|
||
```javascript
|
||
// assets/js/state.js
|
||
|
||
/**
|
||
* Gestionnaire d'état pour le localStorage
|
||
*/
|
||
const AppState = {
|
||
STORAGE_KEY: 'portfolio_contact_form',
|
||
|
||
// Champs à ne jamais stocker
|
||
EXCLUDED_FIELDS: ['csrf_token', 'password', 'recaptcha_token'],
|
||
|
||
/**
|
||
* Vérifie si localStorage est disponible
|
||
*/
|
||
isStorageAvailable() {
|
||
try {
|
||
const test = '__storage_test__';
|
||
localStorage.setItem(test, test);
|
||
localStorage.removeItem(test);
|
||
return true;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Sauvegarde les données du formulaire
|
||
*/
|
||
saveFormData(data) {
|
||
if (!this.isStorageAvailable()) return;
|
||
|
||
try {
|
||
// Filtrer les champs exclus
|
||
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);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Charge les données sauvegardées
|
||
*/
|
||
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;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Efface les données sauvegardées
|
||
*/
|
||
clearFormData() {
|
||
if (!this.isStorageAvailable()) return;
|
||
|
||
try {
|
||
localStorage.removeItem(this.STORAGE_KEY);
|
||
} catch (e) {
|
||
// Silencieux
|
||
}
|
||
}
|
||
};
|
||
```
|
||
|
||
### Intégration dans contact-form.js
|
||
|
||
```javascript
|
||
// Ajouter dans la classe FormValidator ou en complément
|
||
|
||
class ContactFormPersistence {
|
||
constructor(formId) {
|
||
this.form = document.getElementById(formId);
|
||
if (!this.form) return;
|
||
|
||
this.debounceTimer = null;
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
this.loadSavedData();
|
||
this.bindEvents();
|
||
}
|
||
|
||
bindEvents() {
|
||
// Sauvegarder à chaque modification (avec debounce)
|
||
this.form.addEventListener('input', () => {
|
||
clearTimeout(this.debounceTimer);
|
||
this.debounceTimer = setTimeout(() => this.saveData(), 500);
|
||
});
|
||
|
||
// Bouton effacer
|
||
const clearBtn = document.getElementById('clear-form-btn');
|
||
if (clearBtn) {
|
||
clearBtn.addEventListener('click', () => this.clearForm());
|
||
}
|
||
|
||
// Écouter l'envoi réussi
|
||
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];
|
||
}
|
||
});
|
||
|
||
// Mettre à jour le compteur de caractères
|
||
const messageField = this.form.querySelector('[name="message"]');
|
||
const countEl = document.getElementById('message-count');
|
||
if (messageField && countEl) {
|
||
countEl.textContent = messageField.value.length;
|
||
}
|
||
}
|
||
|
||
clearForm() {
|
||
// Confirmation optionnelle
|
||
if (!confirm('Êtes-vous sûr de vouloir effacer le formulaire ?')) {
|
||
return;
|
||
}
|
||
|
||
// Vider le localStorage
|
||
AppState.clearFormData();
|
||
|
||
// Réinitialiser le formulaire
|
||
this.form.reset();
|
||
|
||
// Réinitialiser le compteur
|
||
const countEl = document.getElementById('message-count');
|
||
if (countEl) {
|
||
countEl.textContent = '0';
|
||
}
|
||
|
||
// Effacer les erreurs visuelles
|
||
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 = '';
|
||
});
|
||
}
|
||
}
|
||
|
||
// Initialisation
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
window.contactFormPersistence = new ContactFormPersistence('contact-form');
|
||
});
|
||
```
|
||
|
||
### Clé de Stockage
|
||
|
||
```
|
||
localStorage key: "portfolio_contact_form"
|
||
|
||
Exemple de données stockées:
|
||
{
|
||
"nom": "Dupont",
|
||
"prenom": "Marie",
|
||
"email": "marie@example.com",
|
||
"entreprise": "Acme Corp",
|
||
"categorie": "projet",
|
||
"objet": "Nouveau site web",
|
||
"message": "Bonjour, je souhaite..."
|
||
}
|
||
```
|
||
|
||
### Champs Exclus du Stockage
|
||
|
||
- `csrf_token` (sécurité)
|
||
- `password` (si ajouté)
|
||
- `recaptcha_token` (généré dynamiquement)
|
||
|
||
## Testing
|
||
|
||
- [] Les données sont sauvegardées à la saisie
|
||
- [] Les données sont restaurées au rechargement
|
||
- [] Le bouton "Effacer" vide le formulaire
|
||
- [] Le localStorage est vidé après envoi réussi (événement formSuccess)
|
||
- [] Les champs exclus ne sont pas stockés (EXCLUDED_FIELDS)
|
||
- [] Pas d'erreur si localStorage indisponible (isStorageAvailable)
|
||
- [] Le compteur de caractères est mis à jour au chargement
|
||
|
||
## Dev Agent Record
|
||
|
||
### Agent Model Used
|
||
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 |
|
||
|------|--------|-------------|
|
||
| `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` 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é.
|
||
|
||
## Change Log
|
||
|
||
| Date | Version | Description | Author |
|
||
|------|---------|-------------|--------|
|
||
| 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) |
|