diff --git a/docs/stories/5.1.formulaire-structure-html5.md b/docs/stories/5.1.formulaire-structure-html5.md index 98606d4..c3d28fc 100644 --- a/docs/stories/5.1.formulaire-structure-html5.md +++ b/docs/stories/5.1.formulaire-structure-html5.md @@ -2,7 +2,7 @@ ## Status -Ready for Dev +review ## Story @@ -22,41 +22,41 @@ Ready for Dev ## Tasks / Subtasks -- [] **Task 1 : Créer la page contact.php** (AC: 1) - - [] Mettre à jour `pages/contact.php` - - [] Inclure header, navbar, footer - - [] Route `/contact` déjà configurée (Story 3.2) +- [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) -- [] **Task 2 : Créer la structure du formulaire** (AC: 1, 6) - - [] Balise `
` avec method POST et action - - [] Champ Nom avec label associé (for/id) - - [] Champ Prénom avec label associé - - [] Champ Email avec label associé - - [] Champ Entreprise (optionnel) - - [] Dropdown Catégorie - - [] Champ Objet - - [] Textarea Message +- [x] **Task 2 : Créer la structure du formulaire** (AC: 1, 6) + - [x] Balise `` 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 -- [] **Task 3 : Configurer les attributs HTML5** (AC: 2, 5) - - [] `type="email"` sur le champ email - - [] `required` sur les champs obligatoires - - [] `maxlength` appropriés (100, 255, 200, 5000) - - [] `placeholder` pour guider la saisie - - [] `autocomplete` pour les champs standards +- [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 -- [] **Task 4 : Marquer les champs requis** (AC: 4) - - [] Astérisque visuel sur les labels (span.text-primary) - - [] Indication "(optionnel)" sur entreprise +- [x] **Task 4 : Marquer les champs requis** (AC: 4) + - [x] Astérisque visuel sur les labels (span.text-primary) + - [x] Indication "(optionnel)" sur entreprise -- [] **Task 5 : Configurer le dropdown** (AC: 3) - - [] Option par défaut "Sélectionnez une catégorie..." - - [] 3 options : projet, poste, autre - - [] Attribut `required` +- [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` -- [] **Task 6 : Rendre responsive** (AC: 7) - - [] Grille sm:grid-cols-2 pour Nom/Prénom et Email/Entreprise - - [] Champs empilés sur mobile (grid-cols-1) - - [] Boutons flex-col sur mobile, flex-row sur desktop +- [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 @@ -297,23 +297,29 @@ include_template('navbar', compact('currentPage')); ## Dev Agent Record ### Agent Model Used -Claude Opus 4.5 (claude-opus-4-5-20251101) +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 | Ajout generateCsrfToken() et verifyCsrfToken() | -| `pages/contact.php` | Modified | Formulaire complet avec 7 champs | +| `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 -- Formulaire avec 7 champs : nom, prénom, email, entreprise, catégorie, objet, message -- Token CSRF généré et stocké en session -- Validation HTML5 : required, type="email", maxlength -- Autocomplete sur les champs standards (family-name, given-name, email, organization) -- Layout responsive : 2 colonnes sur desktop, 1 sur mobile -- Compteur de caractères en temps réel pour le message -- Placeholders de messages succès/erreur (pour Story 5.6) -- Spinner de chargement préparé (pour Story 5.2/5.5) +- 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é. @@ -324,3 +330,4 @@ Aucun problème rencontré. |------|---------|-------------|--------| | 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) | diff --git a/includes/functions.php b/includes/functions.php index 58f4f45..05f7c39 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -180,4 +180,26 @@ function getTestimonialByProject(string $projectSlug): ?array } } return null; +} + +/** + * CSRF token helpers + */ +function generateCsrfToken(): string +{ + if (session_status() !== PHP_SESSION_ACTIVE) { + session_start(); + } + if (empty($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); + } + return $_SESSION['csrf_token']; +} + +function verifyCsrfToken(?string $token): bool +{ + if (session_status() !== PHP_SESSION_ACTIVE) { + session_start(); + } + return !empty($token) && !empty($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); } \ No newline at end of file diff --git a/pages/contact.php b/pages/contact.php index 47b3815..d37b547 100644 --- a/pages/contact.php +++ b/pages/contact.php @@ -1,16 +1,157 @@  -
-
-

Contact

-

Le formulaire arrive bientôt.

-
+
+
+
+
+
+

Me Contacter

+

+ Une question, un projet ? Parlons-en ! +

+
+ + + + +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +

+ 0 / 5000 caractères +

+
+ +
+ + +
+ +
+
+
- \ No newline at end of file + + + diff --git a/tests/contact.test.php b/tests/contact.test.php new file mode 100644 index 0000000..a4b8941 --- /dev/null +++ b/tests/contact.test.php @@ -0,0 +1,74 @@ +]*required/', $content) === 1, 'nom missing required'); +assertTrue(preg_match('/id="prenom"[^>]*required/', $content) === 1, 'prenom missing required'); +assertTrue(preg_match('/id="email"[^>]*required/', $content) === 1, 'email missing required'); +assertTrue(preg_match('/id="categorie"[^>]*required/', $content) === 1, 'categorie missing required'); +assertTrue(preg_match('/id="objet"[^>]*required/', $content) === 1, 'objet missing required'); +assertTrue(preg_match('/id="message"[^>]*required/', $content) === 1, 'message missing required'); + +assertTrue(preg_match('/]*id="email"[^>]*type="email"|]*type="email"[^>]*id="email"/', $content) === 1, 'email type not email'); + +assertTrue(preg_match('/id="nom"[^>]*maxlength="100"/', $content) === 1, 'nom maxlength'); +assertTrue(preg_match('/id="prenom"[^>]*maxlength="100"/', $content) === 1, 'prenom maxlength'); +assertTrue(preg_match('/id="email"[^>]*maxlength="255"/', $content) === 1, 'email maxlength'); +assertTrue(preg_match('/id="entreprise"[^>]*maxlength="200"/', $content) === 1, 'entreprise maxlength'); +assertTrue(preg_match('/id="objet"[^>]*maxlength="200"/', $content) === 1, 'objet maxlength'); +assertTrue(preg_match('/id="message"[^>]*maxlength="5000"/', $content) === 1, 'message maxlength'); + +assertTrue(preg_match('/id="nom"[^>]*placeholder="Dupont"/', $content) === 1, 'nom placeholder'); +assertTrue(preg_match('/id="prenom"[^>]*placeholder="Marie"/', $content) === 1, 'prenom placeholder'); +assertTrue(preg_match('/id="email"[^>]*placeholder="marie\\.dupont@example\\.com"/', $content) === 1, 'email placeholder'); +assertTrue(preg_match('/id="entreprise"[^>]*placeholder="Nom de votre entreprise"/', $content) === 1, 'entreprise placeholder'); +assertTrue(preg_match('/id="objet"[^>]*placeholder="Résumez votre demande en quelques mots"/', $content) === 1, 'objet placeholder'); +assertTrue(preg_match('/id="message"[^>]*placeholder="Décrivez votre projet ou votre demande\\.\\.\\."/', $content) === 1, 'message placeholder'); + +assertTrue(preg_match('/id="nom"[^>]*autocomplete="family-name"/', $content) === 1, 'nom autocomplete'); +assertTrue(preg_match('/id="prenom"[^>]*autocomplete="given-name"/', $content) === 1, 'prenom autocomplete'); +assertTrue(preg_match('/id="email"[^>]*autocomplete="email"/', $content) === 1, 'email autocomplete'); +assertTrue(preg_match('/id="entreprise"[^>]*autocomplete="organization"/', $content) === 1, 'entreprise autocomplete'); + +assertTrue(strpos($content, 'text-primary') !== false, 'missing required marker'); +assertTrue(strpos($content, '(optionnel)') !== false, 'missing optional marker'); +assertTrue(strpos($content, 'Sélectionnez une catégorie...') !== false, 'missing categorie placeholder'); +assertTrue(strpos($content, 'Je souhaite parler de mon projet') !== false, 'missing categorie projet'); +assertTrue(strpos($content, 'Je souhaite vous proposer un poste') !== false, 'missing categorie poste'); +assertTrue(strpos($content, 'Autre') !== false, 'missing categorie autre'); +assertTrue(strpos($content, 'grid grid-cols-1 sm:grid-cols-2') !== false, 'missing responsive grid'); +assertTrue(strpos($content, 'flex flex-col sm:flex-row') !== false, 'missing responsive buttons'); + +fwrite(STDOUT, "OK\n"); diff --git a/tests/run.ps1 b/tests/run.ps1 index 6a28d66..87ce452 100644 --- a/tests/run.ps1 +++ b/tests/run.ps1 @@ -1,4 +1,4 @@ -$ErrorActionPreference = 'Stop' +$ErrorActionPreference = 'Stop' $here = Split-Path -Parent $MyInvocation.MyCommand.Path & (Join-Path $here 'structure.test.ps1') & (Join-Path $here 'tailwind.test.ps1') @@ -20,4 +20,5 @@ php (Join-Path $here 'tools.test.php') php (Join-Path $here 'about.test.php') php (Join-Path $here 'passions.test.php') php (Join-Path $here 'testimonials.test.php') -'OK' \ No newline at end of file +php (Join-Path $here 'contact.test.php') +'OK'