Files
Portfolio-Codex/docs/stories/5.1.formulaire-structure-html5.md

13 KiB

Story 5.1: Structure du Formulaire et Validation HTML5

Status

Ready for Dev

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

  • [] 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)
  • [] Task 2 : Créer la structure du formulaire (AC: 1, 6)

    • [] Balise <form> 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
  • [] 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
  • [] Task 4 : Marquer les champs requis (AC: 4)

    • [] Astérisque visuel sur les labels (span.text-primary)
    • [] 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
  • [] 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

Dev Notes

Page pages/contact.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

Claude Opus 4.5 (claude-opus-4-5-20251101)

File List

File Action Description
includes/functions.php Modified Ajout generateCsrfToken() et verifyCsrfToken()
pages/contact.php Modified Formulaire complet avec 7 champs

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)

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)