334 lines
14 KiB
Markdown
334 lines
14 KiB
Markdown
# Story 5.1: Structure du Formulaire et Validation HTML5
|
||
|
||
## Status
|
||
|
||
review
|
||
|
||
## Story
|
||
|
||
**As a** visiteur,
|
||
**I want** un formulaire de contact clair avec des champs bien identifiés,
|
||
**so that** je sais exactement quelles informations fournir.
|
||
|
||
## Acceptance Criteria
|
||
|
||
1. `/contact` affiche le formulaire avec les champs : Nom (requis), Prénom (requis), Email (requis), Entreprise (optionnel), Catégorie (dropdown requis), Objet (requis), Message (textarea requis)
|
||
2. Le champ email utilise `type="email"` pour validation native
|
||
3. Le dropdown Catégorie propose : "Je souhaite parler de mon projet", "Je souhaite vous proposer un poste", "Autre"
|
||
4. Les champs requis sont marqués visuellement (astérisque ou indication)
|
||
5. La validation HTML5 native est activée (required, type="email", maxlength)
|
||
6. Les labels sont explicites et associés aux champs (accessibilité)
|
||
7. Le formulaire est responsive et utilisable sur mobile
|
||
|
||
## Tasks / Subtasks
|
||
|
||
- [x] **Task 1 : Créer la page contact.php** (AC: 1)
|
||
- [x] Mettre à jour `pages/contact.php`
|
||
- [x] Inclure header, navbar, footer
|
||
- [x] Route `/contact` déjà configurée (Story 3.2)
|
||
|
||
- [x] **Task 2 : Créer la structure du formulaire** (AC: 1, 6)
|
||
- [x] Balise `<form>` avec method POST et action
|
||
- [x] Champ Nom avec label associé (for/id)
|
||
- [x] Champ Prénom avec label associé
|
||
- [x] Champ Email avec label associé
|
||
- [x] Champ Entreprise (optionnel)
|
||
- [x] Dropdown Catégorie
|
||
- [x] Champ Objet
|
||
- [x] Textarea Message
|
||
|
||
- [x] **Task 3 : Configurer les attributs HTML5** (AC: 2, 5)
|
||
- [x] `type="email"` sur le champ email
|
||
- [x] `required` sur les champs obligatoires
|
||
- [x] `maxlength` appropriés (100, 255, 200, 5000)
|
||
- [x] `placeholder` pour guider la saisie
|
||
- [x] `autocomplete` pour les champs standards
|
||
|
||
- [x] **Task 4 : Marquer les champs requis** (AC: 4)
|
||
- [x] Astérisque visuel sur les labels (span.text-primary)
|
||
- [x] Indication "(optionnel)" sur entreprise
|
||
|
||
- [x] **Task 5 : Configurer le dropdown** (AC: 3)
|
||
- [x] Option par défaut "Sélectionnez une catégorie..."
|
||
- [x] 3 options : projet, poste, autre
|
||
- [x] Attribut `required`
|
||
|
||
- [x] **Task 6 : Rendre responsive** (AC: 7)
|
||
- [x] Grille sm:grid-cols-2 pour Nom/Prénom et Email/Entreprise
|
||
- [x] Champs empilés sur mobile (grid-cols-1)
|
||
- [x] Boutons flex-col sur mobile, flex-row sur desktop
|
||
|
||
## Dev Notes
|
||
|
||
### Page pages/contact.php
|
||
|
||
```php
|
||
<?php
|
||
/**
|
||
* Page Contact
|
||
*/
|
||
|
||
$pageTitle = 'Contact';
|
||
$pageDescription = 'Contactez-moi pour discuter de votre projet web ou d\'une opportunité professionnelle.';
|
||
$currentPage = 'contact';
|
||
|
||
// Générer le token CSRF
|
||
$csrfToken = generateCsrfToken();
|
||
|
||
include_template('header', compact('pageTitle', 'pageDescription'));
|
||
include_template('navbar', compact('currentPage'));
|
||
?>
|
||
|
||
<main>
|
||
<section class="section">
|
||
<div class="container-content">
|
||
<div class="max-w-2xl mx-auto">
|
||
<!-- Header -->
|
||
<div class="text-center mb-12">
|
||
<h1 class="text-display mb-4">Me Contacter</h1>
|
||
<p class="text-xl text-text-secondary">
|
||
Une question, un projet ? Parlons-en !
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Formulaire -->
|
||
<form
|
||
id="contact-form"
|
||
method="POST"
|
||
action="/api/contact.php"
|
||
class="space-y-6"
|
||
novalidate
|
||
>
|
||
<!-- Token CSRF -->
|
||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken) ?>">
|
||
|
||
<!-- Nom & Prénom (côte à côte sur desktop) -->
|
||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||
<!-- Nom -->
|
||
<div>
|
||
<label for="nom" class="label label-required">Nom</label>
|
||
<input
|
||
type="text"
|
||
id="nom"
|
||
name="nom"
|
||
class="input"
|
||
required
|
||
maxlength="100"
|
||
autocomplete="family-name"
|
||
placeholder="Dupont"
|
||
>
|
||
<p class="error-message hidden" data-error="nom"></p>
|
||
</div>
|
||
|
||
<!-- Prénom -->
|
||
<div>
|
||
<label for="prenom" class="label label-required">Prénom</label>
|
||
<input
|
||
type="text"
|
||
id="prenom"
|
||
name="prenom"
|
||
class="input"
|
||
required
|
||
maxlength="100"
|
||
autocomplete="given-name"
|
||
placeholder="Marie"
|
||
>
|
||
<p class="error-message hidden" data-error="prenom"></p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Email & Entreprise -->
|
||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||
<!-- Email -->
|
||
<div>
|
||
<label for="email" class="label label-required">Email</label>
|
||
<input
|
||
type="email"
|
||
id="email"
|
||
name="email"
|
||
class="input"
|
||
required
|
||
maxlength="255"
|
||
autocomplete="email"
|
||
placeholder="marie.dupont@example.com"
|
||
>
|
||
<p class="error-message hidden" data-error="email"></p>
|
||
</div>
|
||
|
||
<!-- Entreprise (optionnel) -->
|
||
<div>
|
||
<label for="entreprise" class="label">Entreprise <span class="text-text-muted">(optionnel)</span></label>
|
||
<input
|
||
type="text"
|
||
id="entreprise"
|
||
name="entreprise"
|
||
class="input"
|
||
maxlength="200"
|
||
autocomplete="organization"
|
||
placeholder="Nom de votre entreprise"
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Catégorie -->
|
||
<div>
|
||
<label for="categorie" class="label label-required">Catégorie</label>
|
||
<select
|
||
id="categorie"
|
||
name="categorie"
|
||
class="input"
|
||
required
|
||
>
|
||
<option value="" disabled selected>Sélectionnez une catégorie...</option>
|
||
<option value="projet">Je souhaite parler de mon projet</option>
|
||
<option value="poste">Je souhaite vous proposer un poste</option>
|
||
<option value="autre">Autre</option>
|
||
</select>
|
||
<p class="error-message hidden" data-error="categorie"></p>
|
||
</div>
|
||
|
||
<!-- Objet -->
|
||
<div>
|
||
<label for="objet" class="label label-required">Objet</label>
|
||
<input
|
||
type="text"
|
||
id="objet"
|
||
name="objet"
|
||
class="input"
|
||
required
|
||
maxlength="200"
|
||
placeholder="Résumez votre demande en quelques mots"
|
||
>
|
||
<p class="error-message hidden" data-error="objet"></p>
|
||
</div>
|
||
|
||
<!-- Message -->
|
||
<div>
|
||
<label for="message" class="label label-required">Message</label>
|
||
<textarea
|
||
id="message"
|
||
name="message"
|
||
class="textarea"
|
||
required
|
||
maxlength="5000"
|
||
rows="6"
|
||
placeholder="Décrivez votre projet ou votre demande..."
|
||
></textarea>
|
||
<p class="error-message hidden" data-error="message"></p>
|
||
<p class="text-xs text-text-muted mt-1">
|
||
<span id="message-count">0</span> / 5000 caractères
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Boutons -->
|
||
<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">
|
||
<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 type="button" id="clear-form-btn" class="btn-ghost">
|
||
Effacer le formulaire
|
||
</button>
|
||
</div>
|
||
</form>
|
||
|
||
<!-- Message de succès (caché par défaut) -->
|
||
<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é !</h3>
|
||
<p class="text-text-secondary">
|
||
Merci pour votre message. Je vous répondrai dans les meilleurs délais.
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Message d'erreur global (caché par défaut) -->
|
||
<div id="error-message" class="hidden mt-8 p-6 bg-error/10 border border-error/30 rounded-lg">
|
||
<p class="text-error" id="error-text"></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<?php include_template('footer'); ?>
|
||
|
||
<!-- Script du formulaire -->
|
||
<script src="/assets/js/contact-form.js" defer></script>
|
||
```
|
||
|
||
### Attributs des Champs
|
||
|
||
| Champ | Type | Required | Maxlength | Autocomplete |
|
||
|-------|------|----------|-----------|--------------|
|
||
| nom | text | Oui | 100 | family-name |
|
||
| prenom | text | Oui | 100 | given-name |
|
||
| email | email | Oui | 255 | email |
|
||
| entreprise | text | Non | 200 | organization |
|
||
| categorie | select | Oui | - | - |
|
||
| objet | text | Oui | 200 | - |
|
||
| message | textarea | Oui | 5000 | - |
|
||
|
||
### Responsive
|
||
|
||
| Breakpoint | Layout |
|
||
|------------|--------|
|
||
| Mobile | Tous les champs empilés (1 colonne) |
|
||
| Desktop (sm:) | Nom/Prénom côte à côte, Email/Entreprise côte à côte |
|
||
|
||
## Testing
|
||
|
||
- [] Tous les champs sont présents (7 champs)
|
||
- [] Les labels sont associés aux inputs (for/id)
|
||
- [] Les champs requis ont l'astérisque rouge
|
||
- [] La validation HTML5 fonctionne (required)
|
||
- [] Le type="email" valide le format
|
||
- [] Le dropdown a les 3 options + placeholder
|
||
- [] Le formulaire est responsive (grilles adaptatives)
|
||
- [] Le compteur de caractères fonctionne (JS inline)
|
||
|
||
## Dev Agent Record
|
||
|
||
### Agent Model Used
|
||
GPT-5 Codex
|
||
|
||
### Implementation Plan
|
||
- Implémenter les tasks 1 à 6 dans l’ordre avec tests PHP à chaque étape.
|
||
- Mettre à jour `pages/contact.php` progressivement et ajouter le JS du compteur de caractères si requis par les tests.
|
||
|
||
### File List
|
||
| File | Action | Description |
|
||
|------|--------|-------------|
|
||
| `includes/functions.php` | Modified | Fonctions CSRF (génération/validation) |
|
||
| `pages/contact.php` | Modified | Formulaire complet avec validation HTML5 et layout responsive |
|
||
| `tests/contact.test.php` | Added | Tests formulaire contact (structure + attributs) |
|
||
| `tests/run.ps1` | Modified | Ajout du test contact |
|
||
|
||
### Completion Notes
|
||
- Task 1 : page contact initialisée avec header, navbar, footer, et en-tête "Me Contacter"
|
||
- Task 2 : formulaire structuré avec labels associés et champs de base
|
||
- Task 3 : attributs HTML5 (required/type/maxlength/placeholder/autocomplete) configurés
|
||
- Task 4 : labels requis marqués et mention optionnelle ajoutée
|
||
- Task 5 : dropdown catégorie complété avec placeholder et options
|
||
- Task 6 : mise en page responsive en grille et boutons adaptatifs
|
||
- Token CSRF généré et injecté dans le formulaire
|
||
- 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-23 | 1.0 | Implémentation complète | James (Dev) |
|
||
| 2026-02-04 | 1.1 | Formulaire contact HTML5 + responsive | Amelia (Dev) |
|