Files
Portfolio-Codex/docs/stories/5.6.feedback-utilisateur.md

9.6 KiB

Story 5.6: Feedback Utilisateur (Succès/Erreur)

Status

Ready for Dev

Story

As a visiteur, I want savoir clairement si mon message a été envoyé, so that je ne doute pas et j'évite les envois multiples.

Acceptance Criteria

  1. Pendant l'envoi, un indicateur de chargement est affiché (spinner ou texte)
  2. Le bouton d'envoi est désactivé pendant le traitement
  3. En cas de succès : message de confirmation visible, formulaire réinitialisé, localStorage vidé
  4. En cas d'erreur : message d'erreur explicite, données conservées pour réessayer
  5. L'envoi est fait en AJAX (pas de rechargement de page)
  6. Le message de succès invite à vérifier les spams si pas de réponse

Tasks / Subtasks

  • [] Task 1 : Afficher l'état de chargement (AC: 1, 2)

    • [] Masquer le texte du bouton (submitText.classList.add('hidden'))
    • [] Afficher le spinner (submitLoading.classList.remove('hidden'))
    • [] Désactiver le bouton (submitBtn.disabled = true)
  • [] Task 2 : Envoyer en AJAX (AC: 5)

    • [] Utiliser fetch() avec POST
    • [] Envoyer les données en JSON (Content-Type: application/json)
    • [] Inclure les tokens (CSRF + reCAPTCHA)
  • [] Task 3 : Gérer le succès (AC: 3, 6)

    • [] Masquer le formulaire (form.classList.add('hidden'))
    • [] Afficher le message de succès
    • [] Mention des spams (vérifier sous 48h)
    • [] Vider le localStorage (AppState.clearFormData())
    • [] Réinitialiser le formulaire (form.reset())
  • [] Task 4 : Gérer les erreurs (AC: 4)

    • [] Afficher le message d'erreur avec icône
    • [] Garder les données dans le formulaire
    • [] Message "Vos données ont été conservées"
    • [] Permettre de réessayer
  • [] Task 5 : Réinitialiser l'état après feedback

    • [] Masquer le spinner (finally block)
    • [] Réactiver le bouton
    • [] Scroll vers le message (scrollIntoView)

Dev Notes

Code JavaScript Complet

// assets/js/contact-form.js (compléter)

class ContactFormSubmit {
    constructor(formId) {
        this.form = document.getElementById(formId);
        if (!this.form) return;

        this.submitBtn = document.getElementById('submit-btn');
        this.submitText = document.getElementById('submit-text');
        this.submitLoading = document.getElementById('submit-loading');
        this.successMessage = document.getElementById('success-message');
        this.errorMessage = document.getElementById('error-message');
        this.errorText = document.getElementById('error-text');

        this.isSubmitting = false;

        this.init();
    }

    init() {
        // Écouter l'événement de validation réussie
        this.form.addEventListener('validSubmit', () => this.handleSubmit());
    }

    async handleSubmit() {
        if (this.isSubmitting) return;

        this.setLoadingState(true);
        this.hideMessages();

        try {
            // Récupérer les données
            const formData = window.contactFormValidator.getFormData();

            // Ajouter le token CSRF
            const csrfInput = this.form.querySelector('[name="csrf_token"]');
            if (csrfInput) {
                formData.csrf_token = csrfInput.value;
            }

            // Obtenir le token reCAPTCHA
            formData.recaptcha_token = await RecaptchaService.getToken('contact');

            // Envoyer la requête
            const response = await fetch('/api/contact.php', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Accept': 'application/json'
                },
                body: JSON.stringify(formData)
            });

            const result = await response.json();

            if (result.success) {
                this.handleSuccess(result.message);
            } else {
                this.handleError(result.error || 'Une erreur est survenue');
            }

        } catch (error) {
            console.error('Erreur envoi formulaire:', error);
            this.handleError('Impossible de contacter le serveur. Vérifiez votre connexion.');
        } finally {
            this.setLoadingState(false);
        }
    }

    setLoadingState(loading) {
        this.isSubmitting = loading;

        if (this.submitBtn) {
            this.submitBtn.disabled = loading;
        }

        if (this.submitText && this.submitLoading) {
            if (loading) {
                this.submitText.classList.add('hidden');
                this.submitLoading.classList.remove('hidden');
            } else {
                this.submitText.classList.remove('hidden');
                this.submitLoading.classList.add('hidden');
            }
        }
    }

    handleSuccess(message) {
        // Masquer le formulaire
        this.form.classList.add('hidden');

        // Afficher le message de succès
        if (this.successMessage) {
            this.successMessage.classList.remove('hidden');

            // Scroll vers le message
            this.successMessage.scrollIntoView({ behavior: 'smooth', block: 'center' });
        }

        // Vider le localStorage
        AppState.clearFormData();

        // Réinitialiser le formulaire (pour un éventuel nouvel envoi)
        this.form.reset();

        // Déclencher l'événement de succès
        this.form.dispatchEvent(new CustomEvent('formSuccess'));
    }

    handleError(message) {
        // Afficher le message d'erreur
        if (this.errorMessage && this.errorText) {
            this.errorText.textContent = message;
            this.errorMessage.classList.remove('hidden');

            // Scroll vers le message
            this.errorMessage.scrollIntoView({ behavior: 'smooth', block: 'center' });
        }

        // Les données sont conservées dans le formulaire pour réessayer
    }

    hideMessages() {
        if (this.successMessage) {
            this.successMessage.classList.add('hidden');
        }
        if (this.errorMessage) {
            this.errorMessage.classList.add('hidden');
        }
    }
}

// Initialisation
document.addEventListener('DOMContentLoaded', () => {
    window.contactFormSubmit = new ContactFormSubmit('contact-form');
});

HTML des Messages (dans contact.php)

<!-- Message de succès -->
<div id="success-message" class="hidden mt-8 p-6 bg-success/10 border border-success/30 rounded-lg text-center">
    <svg class="w-12 h-12 text-success mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
    </svg>
    <h3 class="text-lg font-semibold text-text-primary mb-2">Message envoyé avec succès !</h3>
    <p class="text-text-secondary mb-4">
        Merci pour votre message. Je vous répondrai dans les meilleurs délais.
    </p>
    <p class="text-sm text-text-muted">
        Si vous ne recevez pas de réponse sous 48h, pensez à vérifier vos spams.
    </p>
</div>

<!-- Message d'erreur -->
<div id="error-message" class="hidden mt-8 p-6 bg-error/10 border border-error/30 rounded-lg">
    <div class="flex items-start gap-4">
        <svg class="w-6 h-6 text-error flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
        </svg>
        <div>
            <h3 class="font-semibold text-error mb-1">Erreur</h3>
            <p class="text-text-secondary" id="error-text"></p>
            <p class="text-sm text-text-muted mt-2">
                Vos données ont été conservées. Vous pouvez réessayer.
            </p>
        </div>
    </div>
</div>

État du Bouton

État Texte Icône Disabled
Normal "Envoyer le message" Aucune Non
Loading "Envoi en cours..." Spinner Oui
Succès (caché) - -
Erreur "Envoyer le message" Aucune Non

Spinner CSS

/* Déjà inclus dans Tailwind avec animate-spin */
.animate-spin {
    animation: spin 1s linear infinite;
}

@keyframes spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
}

Testing

  • [] Le spinner s'affiche pendant l'envoi
  • [] Le bouton est désactivé pendant l'envoi
  • [] Le message de succès s'affiche après envoi réussi
  • [] Le formulaire est masqué après succès
  • [] La mention des spams est présente (48h)
  • [] Le localStorage est vidé après succès
  • [] Le message d'erreur s'affiche si échec
  • [] Les données sont conservées après erreur
  • [] On peut réessayer après une erreur
  • [] Pas de rechargement de page (AJAX)

Dev Agent Record

Agent Model Used

Claude Opus 4.5 (claude-opus-4-5-20251101)

File List

File Action Description
assets/js/contact-form.js Modified Ajout classe ContactFormSubmit
pages/contact.php Modified Messages succès/erreur améliorés

Completion Notes

  • Classe ContactFormSubmit avec gestion complète du cycle de vie
  • État loading : spinner + bouton désactivé
  • Envoi AJAX avec fetch() et JSON
  • Tokens CSRF et reCAPTCHA inclus automatiquement
  • Succès : formulaire masqué, message avec mention spams, localStorage vidé
  • Erreur : message explicite, données conservées, possibilité de réessayer
  • Scroll automatique vers les messages (scrollIntoView smooth)
  • Gestion des erreurs réseau (catch)

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)