Files
Portfolio-Codex/docs/stories/5.6.feedback-utilisateur.md
2026-02-04 21:31:10 +01:00

300 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 lordre 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) |