Files
Portfolio-Codex/docs/stories/5.3.persistance-localstorage.md

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) |