9.6 KiB
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
- Pendant l'envoi, un indicateur de chargement est affiché (spinner ou texte)
- Le bouton d'envoi est désactivé pendant le traitement
- En cas de succès : message de confirmation visible, formulaire réinitialisé, localStorage vidé
- En cas d'erreur : message d'erreur explicite, données conservées pour réessayer
- L'envoi est fait en AJAX (pas de rechargement de page)
- 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) |