Story 1.1: initialisation projet
This commit is contained in:
300
docs/stories/5.3.persistance-localstorage.md
Normal file
300
docs/stories/5.3.persistance-localstorage.md
Normal file
@@ -0,0 +1,300 @@
|
||||
# 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) |
|
||||
Reference in New Issue
Block a user