300 lines
9.9 KiB
Markdown
300 lines
9.9 KiB
Markdown
# Story 5.6: Feedback Utilisateur (Succès/Erreur)
|
||
|
||
## Status
|
||
|
||
review
|
||
|
||
## 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
|
||
|
||
- [x] **Task 1 : Afficher l'état de chargement** (AC: 1, 2)
|
||
- [x] Masquer le texte du bouton (submitText.classList.add('hidden'))
|
||
- [x] Afficher le spinner (submitLoading.classList.remove('hidden'))
|
||
- [x] Désactiver le bouton (submitBtn.disabled = true)
|
||
|
||
- [x] **Task 2 : Envoyer en AJAX** (AC: 5)
|
||
- [x] Utiliser fetch() avec POST
|
||
- [x] Envoyer les données en JSON (Content-Type: application/json)
|
||
- [x] Inclure les tokens (CSRF + reCAPTCHA)
|
||
|
||
- [x] **Task 3 : Gérer le succès** (AC: 3, 6)
|
||
- [x] Masquer le formulaire (form.classList.add('hidden'))
|
||
- [x] Afficher le message de succès
|
||
- [x] Mention des spams (vérifier sous 48h)
|
||
- [x] Vider le localStorage (AppState.clearFormData())
|
||
- [x] Réinitialiser le formulaire (form.reset())
|
||
|
||
- [x] **Task 4 : Gérer les erreurs** (AC: 4)
|
||
- [x] Afficher le message d'erreur avec icône
|
||
- [x] Garder les données dans le formulaire
|
||
- [x] Message "Vos données ont été conservées"
|
||
- [x] Permettre de réessayer
|
||
|
||
- [x] **Task 5 : Réinitialiser l'état après feedback**
|
||
- [x] Masquer le spinner (finally block)
|
||
- [x] Réactiver le bouton
|
||
- [x] Scroll vers le message (scrollIntoView)
|
||
|
||
## Dev Notes
|
||
|
||
### Code JavaScript Complet
|
||
|
||
```javascript
|
||
// 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)
|
||
|
||
```html
|
||
<!-- 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
|
||
|
||
```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
|
||
GPT-5 Codex
|
||
|
||
### Implementation Plan
|
||
- Implémenter les tâches 1 à 5 dans l’ordre avec tests à chaque étape.
|
||
- Ajouter submit JS et messages UX dans contact.php.
|
||
|
||
### File List
|
||
| File | Action | Description |
|
||
|------|--------|-------------|
|
||
| `assets/js/contact-form.js` | Modified | Ajout classe ContactFormSubmit + AJAX |
|
||
| `pages/contact.php` | Modified | Messages succès/erreur + spinner |
|
||
| `tests/contact-submit.test.php` | Added | Tests submit AJAX/UX |
|
||
| `tests/contact.test.php` | Modified | Vérif submit-loading + messages |
|
||
| `tests/run.ps1` | Modified | Ajout du test contact-submit |
|
||
|
||
### Completion Notes
|
||
- Classe ContactFormSubmit avec cycle complet (loading/succès/erreur)
|
||
- Envoi AJAX JSON avec tokens CSRF + reCAPTCHA
|
||
- Succès : formulaire masqué + message 48h + purge localStorage
|
||
- Erreur : message explicite + données conservées
|
||
- Tests : `powershell -ExecutionPolicy Bypass -File tests/run.ps1`
|
||
|
||
### 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) |
|
||
| 2026-02-04 | 1.1 | Feedback utilisateur AJAX | Amelia (Dev) |
|