✨ Story 5.6: feedback utilisateur
This commit is contained in:
@@ -324,8 +324,117 @@ const RecaptchaService = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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() {
|
||||||
|
this.form.addEventListener('validSubmit', () => this.handleSubmit());
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSubmit() {
|
||||||
|
if (this.isSubmitting) return;
|
||||||
|
|
||||||
|
this.setLoadingState(true);
|
||||||
|
this.hideMessages();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = window.contactFormValidator.getFormData();
|
||||||
|
const csrfInput = this.form.querySelector('[name="csrf_token"]');
|
||||||
|
if (csrfInput) {
|
||||||
|
formData.csrf_token = csrfInput.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.recaptcha_token = await RecaptchaService.getToken('contact');
|
||||||
|
|
||||||
|
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) {
|
||||||
|
this.form.classList.add('hidden');
|
||||||
|
|
||||||
|
if (this.successMessage) {
|
||||||
|
this.successMessage.classList.remove('hidden');
|
||||||
|
this.successMessage.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
|
AppState.clearFormData();
|
||||||
|
this.form.reset();
|
||||||
|
this.form.dispatchEvent(new CustomEvent('formSuccess'));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError(message) {
|
||||||
|
if (this.errorMessage && this.errorText) {
|
||||||
|
this.errorText.textContent = message;
|
||||||
|
this.errorMessage.classList.remove('hidden');
|
||||||
|
this.errorMessage.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideMessages() {
|
||||||
|
if (this.successMessage) {
|
||||||
|
this.successMessage.classList.add('hidden');
|
||||||
|
}
|
||||||
|
if (this.errorMessage) {
|
||||||
|
this.errorMessage.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
RecaptchaService.init();
|
RecaptchaService.init();
|
||||||
window.contactFormValidator = new FormValidator('contact-form');
|
window.contactFormValidator = new FormValidator('contact-form');
|
||||||
window.contactFormPersistence = new ContactFormPersistence('contact-form');
|
window.contactFormPersistence = new ContactFormPersistence('contact-form');
|
||||||
|
window.contactFormSubmit = new ContactFormSubmit('contact-form');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Ready for Dev
|
review
|
||||||
|
|
||||||
## Story
|
## Story
|
||||||
|
|
||||||
@@ -21,33 +21,33 @@ Ready for Dev
|
|||||||
|
|
||||||
## Tasks / Subtasks
|
## Tasks / Subtasks
|
||||||
|
|
||||||
- [] **Task 1 : Afficher l'état de chargement** (AC: 1, 2)
|
- [x] **Task 1 : Afficher l'état de chargement** (AC: 1, 2)
|
||||||
- [] Masquer le texte du bouton (submitText.classList.add('hidden'))
|
- [x] Masquer le texte du bouton (submitText.classList.add('hidden'))
|
||||||
- [] Afficher le spinner (submitLoading.classList.remove('hidden'))
|
- [x] Afficher le spinner (submitLoading.classList.remove('hidden'))
|
||||||
- [] Désactiver le bouton (submitBtn.disabled = true)
|
- [x] Désactiver le bouton (submitBtn.disabled = true)
|
||||||
|
|
||||||
- [] **Task 2 : Envoyer en AJAX** (AC: 5)
|
- [x] **Task 2 : Envoyer en AJAX** (AC: 5)
|
||||||
- [] Utiliser fetch() avec POST
|
- [x] Utiliser fetch() avec POST
|
||||||
- [] Envoyer les données en JSON (Content-Type: application/json)
|
- [x] Envoyer les données en JSON (Content-Type: application/json)
|
||||||
- [] Inclure les tokens (CSRF + reCAPTCHA)
|
- [x] Inclure les tokens (CSRF + reCAPTCHA)
|
||||||
|
|
||||||
- [] **Task 3 : Gérer le succès** (AC: 3, 6)
|
- [x] **Task 3 : Gérer le succès** (AC: 3, 6)
|
||||||
- [] Masquer le formulaire (form.classList.add('hidden'))
|
- [x] Masquer le formulaire (form.classList.add('hidden'))
|
||||||
- [] Afficher le message de succès
|
- [x] Afficher le message de succès
|
||||||
- [] Mention des spams (vérifier sous 48h)
|
- [x] Mention des spams (vérifier sous 48h)
|
||||||
- [] Vider le localStorage (AppState.clearFormData())
|
- [x] Vider le localStorage (AppState.clearFormData())
|
||||||
- [] Réinitialiser le formulaire (form.reset())
|
- [x] Réinitialiser le formulaire (form.reset())
|
||||||
|
|
||||||
- [] **Task 4 : Gérer les erreurs** (AC: 4)
|
- [x] **Task 4 : Gérer les erreurs** (AC: 4)
|
||||||
- [] Afficher le message d'erreur avec icône
|
- [x] Afficher le message d'erreur avec icône
|
||||||
- [] Garder les données dans le formulaire
|
- [x] Garder les données dans le formulaire
|
||||||
- [] Message "Vos données ont été conservées"
|
- [x] Message "Vos données ont été conservées"
|
||||||
- [] Permettre de réessayer
|
- [x] Permettre de réessayer
|
||||||
|
|
||||||
- [] **Task 5 : Réinitialiser l'état après feedback**
|
- [x] **Task 5 : Réinitialiser l'état après feedback**
|
||||||
- [] Masquer le spinner (finally block)
|
- [x] Masquer le spinner (finally block)
|
||||||
- [] Réactiver le bouton
|
- [x] Réactiver le bouton
|
||||||
- [] Scroll vers le message (scrollIntoView)
|
- [x] Scroll vers le message (scrollIntoView)
|
||||||
|
|
||||||
## Dev Notes
|
## Dev Notes
|
||||||
|
|
||||||
@@ -265,23 +265,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
## Dev Agent Record
|
## Dev Agent Record
|
||||||
|
|
||||||
### Agent Model Used
|
### Agent Model Used
|
||||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
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 List
|
||||||
| File | Action | Description |
|
| File | Action | Description |
|
||||||
|------|--------|-------------|
|
|------|--------|-------------|
|
||||||
| `assets/js/contact-form.js` | Modified | Ajout classe ContactFormSubmit |
|
| `assets/js/contact-form.js` | Modified | Ajout classe ContactFormSubmit + AJAX |
|
||||||
| `pages/contact.php` | Modified | Messages succès/erreur améliorés |
|
| `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
|
### Completion Notes
|
||||||
- Classe ContactFormSubmit avec gestion complète du cycle de vie
|
- Classe ContactFormSubmit avec cycle complet (loading/succès/erreur)
|
||||||
- État loading : spinner + bouton désactivé
|
- Envoi AJAX JSON avec tokens CSRF + reCAPTCHA
|
||||||
- Envoi AJAX avec fetch() et JSON
|
- Succès : formulaire masqué + message 48h + purge localStorage
|
||||||
- Tokens CSRF et reCAPTCHA inclus automatiquement
|
- Erreur : message explicite + données conservées
|
||||||
- Succès : formulaire masqué, message avec mention spams, localStorage vidé
|
- Tests : `powershell -ExecutionPolicy Bypass -File tests/run.ps1`
|
||||||
- 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
|
### Debug Log References
|
||||||
Aucun problème rencontré.
|
Aucun problème rencontré.
|
||||||
@@ -292,3 +296,4 @@ Aucun problème rencontré.
|
|||||||
|------|---------|-------------|--------|
|
|------|---------|-------------|--------|
|
||||||
| 2026-01-22 | 0.1 | Création initiale | Sarah (PO) |
|
| 2026-01-22 | 0.1 | Création initiale | Sarah (PO) |
|
||||||
| 2026-01-24 | 1.0 | Implémentation complète | James (Dev) |
|
| 2026-01-24 | 1.0 | Implémentation complète | James (Dev) |
|
||||||
|
| 2026-02-04 | 1.1 | Feedback utilisateur AJAX | Amelia (Dev) |
|
||||||
|
|||||||
@@ -134,13 +134,48 @@ include_template('navbar', compact('currentPage'));
|
|||||||
|
|
||||||
<div class="flex flex-col sm:flex-row gap-4 pt-4">
|
<div class="flex flex-col sm:flex-row gap-4 pt-4">
|
||||||
<button type="submit" id="submit-btn" class="btn-primary flex-1 justify-center">
|
<button type="submit" id="submit-btn" class="btn-primary flex-1 justify-center">
|
||||||
Envoyer le message
|
<span id="submit-text">Envoyer le message</span>
|
||||||
|
<span id="submit-loading" class="hidden">
|
||||||
|
<svg class="animate-spin w-5 h-5" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||||
|
</svg>
|
||||||
|
Envoi en cours...
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" id="clear-form-btn" class="btn-ghost">
|
<button type="button" id="clear-form-btn" class="btn-ghost">
|
||||||
Effacer le formulaire
|
Effacer le formulaire
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
19
tests/contact-submit.test.php
Normal file
19
tests/contact-submit.test.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
function assertTrue($cond, $msg) {
|
||||||
|
if (!$cond) {
|
||||||
|
fwrite(STDERR, $msg . PHP_EOL);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents(__DIR__ . '/../assets/js/contact-form.js');
|
||||||
|
|
||||||
|
assertTrue(strpos($content, 'class ContactFormSubmit') !== false, 'missing ContactFormSubmit');
|
||||||
|
assertTrue(strpos($content, 'fetch(') !== false, 'missing fetch');
|
||||||
|
assertTrue(strpos($content, 'submit-text') !== false, 'missing submit-text');
|
||||||
|
assertTrue(strpos($content, 'submit-loading') !== false, 'missing submit-loading');
|
||||||
|
assertTrue(strpos($content, 'setLoadingState') !== false, 'missing loading state');
|
||||||
|
assertTrue(strpos($content, 'formSuccess') !== false, 'missing formSuccess dispatch');
|
||||||
|
assertTrue(strpos($content, 'AppState.clearFormData') !== false, 'missing localStorage clear');
|
||||||
|
|
||||||
|
fwrite(STDOUT, "OK\n");
|
||||||
@@ -41,6 +41,11 @@ assertTrue(strpos($content, 'data-error="objet"') !== false, 'missing objet erro
|
|||||||
assertTrue(strpos($content, 'data-error="message"') !== false, 'missing message error');
|
assertTrue(strpos($content, 'data-error="message"') !== false, 'missing message error');
|
||||||
assertTrue(strpos($content, 'id="submit-btn"') !== false, 'missing submit id');
|
assertTrue(strpos($content, 'id="submit-btn"') !== false, 'missing submit id');
|
||||||
assertTrue(strpos($content, 'id="clear-form-btn"') !== false, 'missing clear button id');
|
assertTrue(strpos($content, 'id="clear-form-btn"') !== false, 'missing clear button id');
|
||||||
|
assertTrue(strpos($content, 'id="submit-text"') !== false, 'missing submit text');
|
||||||
|
assertTrue(strpos($content, 'id="submit-loading"') !== false, 'missing submit loading');
|
||||||
|
assertTrue(strpos($content, 'id="success-message"') !== false, 'missing success message');
|
||||||
|
assertTrue(strpos($content, 'id="error-message"') !== false, 'missing error message');
|
||||||
|
assertTrue(strpos($content, 'id="error-text"') !== false, 'missing error text');
|
||||||
assertTrue(strpos($content, '/assets/js/state.js') !== false, 'missing state script');
|
assertTrue(strpos($content, '/assets/js/state.js') !== false, 'missing state script');
|
||||||
assertTrue(strpos($content, '/assets/js/contact-form.js') !== false, 'missing contact script');
|
assertTrue(strpos($content, '/assets/js/contact-form.js') !== false, 'missing contact script');
|
||||||
|
|
||||||
|
|||||||
@@ -25,4 +25,5 @@ php (Join-Path $here 'contact-validation.test.php')
|
|||||||
php (Join-Path $here 'contact-state.test.php')
|
php (Join-Path $here 'contact-state.test.php')
|
||||||
php (Join-Path $here 'recaptcha.test.php')
|
php (Join-Path $here 'recaptcha.test.php')
|
||||||
php (Join-Path $here 'contact-api.test.php')
|
php (Join-Path $here 'contact-api.test.php')
|
||||||
|
php (Join-Path $here 'contact-submit.test.php')
|
||||||
'OK'
|
'OK'
|
||||||
|
|||||||
Reference in New Issue
Block a user