301 lines
8.4 KiB
Markdown
301 lines
8.4 KiB
Markdown
# Story 5.3: Persistance des Données avec localStorage
|
|
|
|
## Status
|
|
|
|
Ready for Dev
|
|
|
|
## 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
|
|
|
|
- [] **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)
|
|
|
|
- [] **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
|
|
|
|
- [] **Task 3 : Restaurer au chargement** (AC: 2)
|
|
- [] Charger les données au DOMContentLoaded
|
|
- [] Pré-remplir chaque champ
|
|
- [] 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
|
|
|
|
- [] **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()
|
|
|
|
- [] **Task 6 : Exclure les données sensibles** (AC: 5)
|
|
- [] Ne pas stocker csrf_token, password, recaptcha_token
|
|
- [] 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
|
|
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
|
|
|
### 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 |
|
|
|
|
### 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
|
|
|
|
### 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) |
|