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

8.4 KiB

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)

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

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