Story 1.1: initialisation projet
This commit is contained in:
14
.env.example
Normal file
14
.env.example
Normal file
@@ -0,0 +1,14 @@
|
||||
# Application
|
||||
APP_ENV=development
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost:8000
|
||||
|
||||
# reCAPTCHA v3
|
||||
RECAPTCHA_SITE_KEY=your_site_key_here
|
||||
RECAPTCHA_SECRET_KEY=your_secret_key_here
|
||||
|
||||
# Contact Email
|
||||
CONTACT_EMAIL=contact@example.com
|
||||
|
||||
# Securite
|
||||
APP_SECRET=your_random_secret_key_here
|
||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
.env
|
||||
vendor/
|
||||
node_modules/
|
||||
logs/*.log
|
||||
assets/css/output.css
|
||||
.idea/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
_bmad/
|
||||
9
composer.json
Normal file
9
composer.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "portfolio/website",
|
||||
"description": "Portfolio developpeur web",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=8.0",
|
||||
"vlucas/phpdotenv": "^5.6"
|
||||
}
|
||||
}
|
||||
356
docs/brainstorming-session-results.md
Normal file
356
docs/brainstorming-session-results.md
Normal file
@@ -0,0 +1,356 @@
|
||||
# Brainstorming Session - Portfolio Developpeur
|
||||
|
||||
**Date** : 22 janvier 2026
|
||||
**Facilitatrice** : Mary (Business Analyst)
|
||||
**Technique utilisee** : Premiers Principes (First Principles Thinking)
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
| Element | Detail |
|
||||
|---------|---------------------------------------------------------------------------|
|
||||
| **Sujet** | Portfolio developpeur web |
|
||||
| **Objectifs** | Convaincre recruteurs & clients, inspirer confiance, obtenir des contacts |
|
||||
| **Contraintes** | HTML/CSS/JS pur, simplicite, optimisation, utilisation de Tailwind CSS |
|
||||
| **Principe central** | **"Montrer plutot que dire"** |
|
||||
|
||||
### Vision du Portfolio
|
||||
|
||||
Un portfolio qui :
|
||||
- Permet de connaitre la personne
|
||||
- Cree une proximite professionnelle
|
||||
- Met en avant le travail accompli
|
||||
- Inspire la confiance par des preuves tangibles
|
||||
- Incite a la prise de contact
|
||||
|
||||
---
|
||||
|
||||
## Architecture Globale
|
||||
|
||||
```
|
||||
PORTFOLIO
|
||||
|
||||
+-- PAGE D'ACCUEIL
|
||||
| +-- Accroche + navigation claire
|
||||
|
|
||||
+-- PROJETS
|
||||
| +-- 5-10 projets vedettes (pages dediees)
|
||||
| +-- Liste projets secondaires
|
||||
|
|
||||
+-- COMPETENCES & OUTILS
|
||||
| +-- Technos de dev -> liees aux projets
|
||||
| +-- Outils demontrables -> liens/preuves
|
||||
| +-- Outils non demontrables -> liste + contexte
|
||||
|
|
||||
+-- ME DECOUVRIR
|
||||
| +-- Parcours / etudes
|
||||
| +-- Le "pourquoi" du metier
|
||||
| +-- Passions hors travail
|
||||
|
|
||||
+-- TEMOIGNAGES
|
||||
| +-- Avis clients / employeurs
|
||||
|
|
||||
+-- CONTACT
|
||||
| +-- Formulaire principal
|
||||
| +-- Liens secondaires (reseaux)
|
||||
|
|
||||
+-- NAVBAR
|
||||
+-- Bouton "Me contacter" visible
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Confiance & Credibilite
|
||||
|
||||
### Principe : L'Authenticite Verifiable
|
||||
|
||||
> "Les mots sont bien pour expliquer mais pour montrer que l'on maitrise, le mieux reste de pouvoir naviguer et interagir avec les projets."
|
||||
|
||||
### Elements de preuve
|
||||
|
||||
| Element | Ce que ca prouve |
|
||||
|---------|------------------|
|
||||
| Lien vers le projet en ligne | Le projet existe reellement |
|
||||
| Projet visitable | Les technos sont verifiables |
|
||||
| Explication technique | C'est bien vous qui l'avez fait |
|
||||
| Temoignages clients/employeurs | Professionnalisme confirme par des tiers |
|
||||
|
||||
### Ce qui construit la confiance
|
||||
|
||||
- Projets visitables en ligne
|
||||
- Technologies utilisees visibles et verifiables
|
||||
- Explications techniques des fonctionnalites (preuve de paternite)
|
||||
- Temoignages de clients ou employeurs
|
||||
- Authenticite totale - pas de mensonges
|
||||
|
||||
### Ce qui detruit la confiance
|
||||
|
||||
- Mensonges sur les projets
|
||||
- Fausses affirmations sur les technologies
|
||||
- Promesses sans demonstration
|
||||
|
||||
---
|
||||
|
||||
## 2. Connexion Humaine
|
||||
|
||||
### Principe : Incarner plutot qu'affirmer
|
||||
|
||||
> "N'importe qui peut ecrire 'Je suis un bon communicant'. Comment le DEMONTRER ?"
|
||||
|
||||
### Comment creer la connexion
|
||||
|
||||
| Qualite | Comment la MONTRER |
|
||||
|---------|-------------------|
|
||||
| Communication | Le ton et le style de redaction du portfolio |
|
||||
| Passions | Mentionner + preuves visuelles (photos d'evenements) |
|
||||
| Professionnalisme | Section temoignages clients/employeurs |
|
||||
|
||||
### Montrer la passion sans le dire
|
||||
|
||||
Au lieu de dire "Je suis passionne", montrer :
|
||||
- Projets personnels en plus des projets pro
|
||||
- Veille tech active sur l'actualite
|
||||
- 5+ ans dans le domaine (engagement long terme)
|
||||
|
||||
---
|
||||
|
||||
## 3. Design & UX
|
||||
|
||||
### Principe : Le design au service du message
|
||||
|
||||
| Ce qu'il faut | Ce qu'il faut eviter |
|
||||
|---------------|---------------------|
|
||||
| Animations subtiles | Trop d'effets = distraction |
|
||||
| Se demarquer visuellement | Style au detriment du contenu |
|
||||
| Design soigne | Impressionner pour impressionner |
|
||||
| Pages aerees | Paves de texte |
|
||||
| Ton sympathique | Trop formel ou trop familier |
|
||||
|
||||
---
|
||||
|
||||
## 4. Competences & Outils
|
||||
|
||||
### Principe : Adapter la preuve a l'outil
|
||||
|
||||
> "Les barres de progression ne repondent pas a la vraie question : 'Peut-il livrer MON projet ?'"
|
||||
|
||||
### Strategie par type
|
||||
|
||||
| Type d'outil | Comment le presenter |
|
||||
|--------------|---------------------|
|
||||
| **Technos de dev** (HTML, CSS, JS...) | Liees aux projets (preuves vivantes) |
|
||||
| **Outils demontrables** (Git, Notion...) | Liens vers preuves concretes (depots, pages) |
|
||||
| **Outils non demontrables** (IDE...) | Liste + explication de l'usage concret |
|
||||
|
||||
### Exemples concrets
|
||||
|
||||
- **Git** : Liens vers depots open-source
|
||||
- **Notion** : Page de gestion de projet visible
|
||||
- **PHPStorm** : Description de l'usage dans l'environnement de travail
|
||||
|
||||
---
|
||||
|
||||
## 5. Presentation des Projets
|
||||
|
||||
### Structure d'une page projet
|
||||
|
||||
```
|
||||
PAGE PROJET
|
||||
|
||||
+-- CONTEXTE
|
||||
| +-- Quand, pour qui, pourquoi
|
||||
| +-- Besoins et contraintes
|
||||
|
|
||||
+-- SOLUTION TECHNIQUE
|
||||
| +-- Technologies choisies
|
||||
| +-- Argumentation des choix
|
||||
|
|
||||
+-- TRAVAIL D'EQUIPE (si applicable)
|
||||
| +-- Votre role / contribution
|
||||
| +-- Organisation de la communication
|
||||
| +-- Outils utilises (imposes ou initiative)
|
||||
| +-- Ce que vous avez simplifie
|
||||
|
|
||||
+-- REALISATION
|
||||
| +-- Duree du projet
|
||||
|
|
||||
+-- RESULTATS (optionnel)
|
||||
| +-- Temoignage client sur les benefices
|
||||
|
|
||||
+-- DIFFICULTES (secondaire)
|
||||
| +-- Problemes rencontres et solutions
|
||||
|
|
||||
+-- VOIR LE PROJET
|
||||
+-- Lien vers le site/demo
|
||||
+-- Ou captures d'ecran si non disponible
|
||||
```
|
||||
|
||||
### Strategie de selection
|
||||
|
||||
| Niveau | Format | Criteres |
|
||||
|--------|--------|----------|
|
||||
| **Projets vedettes** (5-10) | Page dediee complete | Fini/en cours, demo dispo, valeur en competences |
|
||||
| **Projets secondaires** | Liste simple | Petits projets, sujets deja couverts, peu de valeur ajoutee |
|
||||
|
||||
### Criteres pour un projet vedette
|
||||
|
||||
- Projet fini ou en cours actif (pas avorte)
|
||||
- Demo disponible (idealement)
|
||||
- A permis de resoudre des problemes concrets
|
||||
- Ou gestion/organisation interessante
|
||||
- = Valeur en competences acquises
|
||||
|
||||
---
|
||||
|
||||
## 6. Page "Me Decouvrir"
|
||||
|
||||
### Equilibre personnel/professionnel
|
||||
|
||||
| OK de partager | Trop personnel | Trop generique |
|
||||
|----------------|----------------|----------------|
|
||||
| Passions / hobbies | Localite precise | "Je suis passionne" |
|
||||
| Ce qui vide la tete | Environnement familial | Phrases bateau |
|
||||
| Parcours / etudes | | Cliches du metier |
|
||||
| Le "pourquoi" du metier | | |
|
||||
|
||||
### Structure de la page
|
||||
|
||||
```
|
||||
PAGE A PROPOS
|
||||
|
||||
+-- QUI JE SUIS
|
||||
| +-- Ton sympathique, pas trop formel
|
||||
| +-- Texte concis et aere
|
||||
| +-- Grande ville (pas adresse precise)
|
||||
|
|
||||
+-- MON PARCOURS
|
||||
| +-- Etudes / Formation
|
||||
| +-- Ce qui m'a amene au dev web
|
||||
| +-- 5+ ans d'experience (longevite)
|
||||
|
|
||||
+-- POURQUOI CE METIER
|
||||
| +-- Histoire personnelle authentique
|
||||
| +-- Pas de cliches
|
||||
|
|
||||
+-- EN DEHORS DU CODE
|
||||
| +-- Passions / hobbies
|
||||
| +-- Ce qui me vide la tete
|
||||
| +-- Preuves visuelles si possible
|
||||
|
|
||||
+-- PREUVES DE PASSION (implicites)
|
||||
+-- Projets perso visibles
|
||||
+-- Veille tech / actualites
|
||||
+-- Engagement sur la duree
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Contact & Call-to-Action
|
||||
|
||||
### Principe : Zero friction
|
||||
|
||||
> "Ce qui pourrait empecher quelqu'un de convaincu de me contacter sont les limites qui cassent la fluidite."
|
||||
|
||||
### Strategie CTA
|
||||
|
||||
| Element | Implementation |
|
||||
|---------|----------------|
|
||||
| **Bouton navbar** | Style rempli, se detache, toujours visible |
|
||||
| **Page contact** | Dediee (pas modale) pour persistance |
|
||||
| **Formulaire** | Labels clairs, zero reflexion |
|
||||
| **Liens secondaires** | LinkedIn, Instagram, GitHub, mailto |
|
||||
|
||||
### Structure du formulaire
|
||||
|
||||
| Champ | Type | Obligatoire |
|
||||
|-------|------|-------------|
|
||||
| Nom | Texte | Oui |
|
||||
| Prenom | Texte | Oui |
|
||||
| Entreprise | Texte | Non |
|
||||
| Categorie | Dropdown | Oui |
|
||||
| Objet | Texte | Oui |
|
||||
| Message | Zone de texte | Oui |
|
||||
| Captcha | reCAPTCHA v3 invisible | Auto |
|
||||
|
||||
### Categories de contact
|
||||
|
||||
1. "Je souhaite parler de mon projet" (Client/Freelance)
|
||||
2. "Je souhaite vous proposer un poste" (Recruteur/Job)
|
||||
3. "Autre"
|
||||
|
||||
### Points cles
|
||||
|
||||
- Persistance des donnees si le visiteur quitte et revient
|
||||
- reCAPTCHA invisible (pas de friction)
|
||||
- Formulaire = primaire, liens reseaux = secondaire
|
||||
|
||||
---
|
||||
|
||||
## Plan d'Action
|
||||
|
||||
### Priorites Immediates
|
||||
|
||||
| # | Action |
|
||||
|---|--------|
|
||||
| 1 | Selectionner 5-10 projets vedettes selon les criteres |
|
||||
| 2 | Structurer chaque projet (contexte -> solution -> resultat) |
|
||||
| 3 | Rediger la page "Me decouvrir" avec ton authentique |
|
||||
| 4 | Creer le formulaire de contact avec persistance des donnees |
|
||||
| 5 | Integrer le bouton CTA dans la navbar |
|
||||
|
||||
### Developpements Futurs
|
||||
|
||||
| # | Action |
|
||||
|---|--------|
|
||||
| 1 | Collecter temoignages clients/employeurs |
|
||||
| 2 | Documenter les outils demontrables (depots Git public, etc.) |
|
||||
| 3 | Ajouter preuves visuelles des passions |
|
||||
| 4 | Creer liste des projets secondaires |
|
||||
|
||||
---
|
||||
|
||||
## Principes Cles a Retenir
|
||||
|
||||
### 1. MONTRER PLUTOT QUE DIRE
|
||||
Chaque affirmation doit avoir une preuve tangible.
|
||||
|
||||
### 2. ADAPTER LA PREUVE A L'ELEMENT
|
||||
- Projets = liens visitables
|
||||
- Outils = demonstrations concretes
|
||||
- Soft skills = temoignages tiers
|
||||
|
||||
### 3. ZERO FRICTION
|
||||
Du premier clic jusqu'au contact, aucun obstacle.
|
||||
|
||||
### 4. AUTHENTICITE VERIFIABLE
|
||||
Pas de mensonges, tout peut etre verifie par le visiteur.
|
||||
|
||||
### 5. EQUILIBRE PERSONNEL/PROFESSIONNEL
|
||||
Humain et accessible sans etre intrusif.
|
||||
|
||||
---
|
||||
|
||||
## Reflexion & Suivi
|
||||
|
||||
### Ce qui a bien fonctionne dans cette session
|
||||
|
||||
- La technique des Premiers Principes a permis de deconstruire chaque aspect
|
||||
- Le principe "Montrer plutot que dire" est devenu le fil conducteur
|
||||
- Vision coherente entre tous les elements du portfolio
|
||||
|
||||
### Pistes pour exploration future
|
||||
|
||||
- Tests utilisateurs une fois le portfolio cree
|
||||
- A/B testing sur les CTA
|
||||
- Analyse des taux de conversion du formulaire
|
||||
|
||||
### Questions emergentes
|
||||
|
||||
- Comment mesurer l'efficacite du portfolio ?
|
||||
- Quelle frequence de mise a jour des projets ?
|
||||
- Comment gerer les projets confidentiels ?
|
||||
|
||||
---
|
||||
|
||||
*Document genere lors d'une session de brainstorming facilitee par Mary (Business Analyst) - Methode BMAD*
|
||||
784
docs/front-end-spec.md
Normal file
784
docs/front-end-spec.md
Normal file
@@ -0,0 +1,784 @@
|
||||
# Portfolio Développeur Web - Spécification UI/UX
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
Ce document définit les objectifs d'expérience utilisateur, l'architecture de l'information, les flux utilisateurs et les spécifications de design visuel pour l'interface utilisateur du **Portfolio Développeur Web**. Il sert de fondation pour le design visuel et le développement frontend, assurant une expérience cohérente et centrée sur l'utilisateur.
|
||||
|
||||
### 1.1 Objectifs UX & Principes de Design
|
||||
|
||||
#### Personas Utilisateurs Cibles
|
||||
|
||||
| Persona | Description |
|
||||
|---------|-------------|
|
||||
| **Recruteur** | Professionnel RH ou technique cherchant un candidat fiable, compétent, avec des preuves vérifiables de ses compétences |
|
||||
| **Client Potentiel** | Entrepreneur ou responsable projet souhaitant évaluer la capacité à livrer un projet freelance, sensible à la qualité et au professionnalisme |
|
||||
| **Curieux / Pair** | Développeur ou professionnel tech consultant le portfolio par curiosité ou benchmark |
|
||||
|
||||
#### Objectifs d'Utilisabilité
|
||||
|
||||
- **Découverte rapide** : Comprendre en <10 secondes qui est le développeur et ce qu'il propose
|
||||
- **Navigation intuitive** : Trouver n'importe quelle section en maximum 2 clics
|
||||
- **Zéro friction** : Aucun obstacle du premier clic jusqu'au formulaire de contact envoyé
|
||||
- **Preuves accessibles** : Chaque compétence/affirmation liée à une preuve cliquable
|
||||
- **Mobile-first** : Expérience optimale sur smartphone (prioritaire)
|
||||
|
||||
#### Principes de Design
|
||||
|
||||
1. **Le design au service du message** — L'interface reste au second plan, le contenu prime
|
||||
2. **Montrer plutôt que dire** — Chaque affirmation soutenue par une preuve tangible
|
||||
3. **Progressive disclosure** — Projets vedettes en détail, projets secondaires en liste
|
||||
4. **Feedback immédiat** — Toute action a une réponse visuelle claire
|
||||
5. **Accessible par défaut** — Contrastes, navigation clavier, structure sémantique
|
||||
|
||||
### 1.2 Journal des modifications
|
||||
|
||||
| Date | Version | Description | Auteur |
|
||||
|------|---------|-------------|--------|
|
||||
| 2026-01-22 | 0.1 | Création initiale | Sally (UX Expert) |
|
||||
|
||||
## 2. Architecture de l'Information (IA)
|
||||
|
||||
### 2.1 Site Map / Inventaire des Écrans
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[🏠 Accueil] --> B[📁 Projets]
|
||||
A --> C[🛠 Compétences]
|
||||
A --> D[👤 Me Découvrir]
|
||||
A --> E[✉️ Contact]
|
||||
|
||||
B --> B1[Projets Vedettes]
|
||||
B --> B2[Projets Secondaires]
|
||||
B1 --> B3[Page Projet 1]
|
||||
B1 --> B4[Page Projet 2]
|
||||
B1 --> B5[Page Projet N...]
|
||||
|
||||
C --> C1[Technologies de Dev]
|
||||
C --> C2[Outils Démontrables]
|
||||
C --> C3[Autres Outils]
|
||||
|
||||
D --> D1[Parcours]
|
||||
D --> D2[Motivations]
|
||||
D --> D3[Passions]
|
||||
D --> D4[Témoignages]
|
||||
|
||||
E --> E1[Formulaire]
|
||||
E --> E2[Liens Secondaires]
|
||||
```
|
||||
|
||||
### 2.2 Structure de Navigation
|
||||
|
||||
**Navigation Primaire (Navbar sticky)** :
|
||||
|
||||
| Élément | URL | Notes |
|
||||
|---------|-----|-------|
|
||||
| Accueil | `/` | Logo cliquable |
|
||||
| Projets | `/projets` | Liste vedettes + secondaires |
|
||||
| Compétences | `/competences` | Technos + outils |
|
||||
| Me Découvrir | `/a-propos` | Parcours + témoignages |
|
||||
| **Me Contacter** | `/contact` | **CTA bouton distinct** |
|
||||
|
||||
**Navigation Secondaire** :
|
||||
- **Footer** : Liens réseaux sociaux (LinkedIn, GitHub), mentions légales
|
||||
- **Breadcrumbs** : Sur pages projet uniquement (`Accueil > Projets > Nom du projet`)
|
||||
|
||||
**Stratégie de Breadcrumb** :
|
||||
- Activé uniquement sur les pages projet individuelles
|
||||
- Format : Texte cliquable pour chaque niveau
|
||||
- Dernier élément (page actuelle) non cliquable
|
||||
|
||||
## 3. Flux Utilisateurs
|
||||
|
||||
### 3.1 Découverte et Exploration d'un Projet
|
||||
|
||||
**Objectif utilisateur** : Évaluer les compétences du développeur à travers un projet concret
|
||||
|
||||
**Points d'entrée** :
|
||||
- Page d'accueil (CTA "Découvrir mes projets")
|
||||
- Navbar (lien "Projets")
|
||||
- Page Compétences (lien technologie → projets associés)
|
||||
|
||||
**Critères de succès** : L'utilisateur visite le projet en ligne ou consulte les captures d'écran
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Entrée: Accueil/Navbar] --> B[Page Projets]
|
||||
B --> C{Choix du projet}
|
||||
C --> D[Page Projet Détaillée]
|
||||
D --> E[Lecture: Contexte]
|
||||
E --> F[Lecture: Solution Technique]
|
||||
F --> G[Lecture: Réalisation]
|
||||
G --> H{Projet en ligne?}
|
||||
H -->|Oui| I[🔗 Visite du projet]
|
||||
H -->|Non| J[📷 Galerie captures]
|
||||
I --> K{Convaincu?}
|
||||
J --> K
|
||||
K -->|Oui| L[CTA: Me Contacter]
|
||||
K -->|Non| M[Retour liste projets]
|
||||
M --> C
|
||||
```
|
||||
|
||||
**Edge Cases & Gestion d'erreurs** :
|
||||
- Projet hors ligne : Afficher "Projet non disponible" + captures d'écran
|
||||
- Lien externe cassé : Vérification périodique, fallback vers captures
|
||||
- Projet confidentiel : Mention "Projet sous NDA" + description anonymisée
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Prise de Contact (Conversion)
|
||||
|
||||
**Objectif utilisateur** : Envoyer un message au développeur
|
||||
|
||||
**Points d'entrée** :
|
||||
- Bouton CTA navbar (toutes pages)
|
||||
- Fin de page projet
|
||||
- Fin de page "Me Découvrir"
|
||||
|
||||
**Critères de succès** : Message envoyé avec confirmation visible
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[CTA: Me Contacter] --> B[Page Contact]
|
||||
B --> C[Remplissage formulaire]
|
||||
C --> D{Données persistées?}
|
||||
D -->|Oui| E[Pré-remplissage auto]
|
||||
D -->|Non| F[Formulaire vierge]
|
||||
E --> G[Complétion]
|
||||
F --> G
|
||||
G --> H[Validation temps réel]
|
||||
H --> I{Erreurs?}
|
||||
I -->|Oui| J[Affichage erreurs inline]
|
||||
J --> G
|
||||
I -->|Non| K[Clic Envoyer]
|
||||
K --> L[Spinner + bouton désactivé]
|
||||
L --> M{reCAPTCHA OK?}
|
||||
M -->|Non| N[Erreur discrète, retry]
|
||||
M -->|Oui| O[Envoi PHP]
|
||||
O --> P{Succès?}
|
||||
P -->|Oui| Q[✅ Message de confirmation]
|
||||
P -->|Non| R[❌ Erreur + données conservées]
|
||||
Q --> S[localStorage vidé]
|
||||
R --> G
|
||||
```
|
||||
|
||||
**Edge Cases & Gestion d'erreurs** :
|
||||
- Utilisateur quitte et revient : Données restaurées depuis localStorage
|
||||
- reCAPTCHA échoue : Dégradation gracieuse, formulaire reste fonctionnel
|
||||
- Erreur serveur : Message explicite, données préservées pour retry
|
||||
- Double soumission : Bouton désactivé pendant traitement
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Vérification des Compétences
|
||||
|
||||
**Objectif utilisateur** : Valider que le développeur maîtrise une technologie spécifique
|
||||
|
||||
**Points d'entrée** :
|
||||
- Navbar (lien "Compétences")
|
||||
- Badge technologie sur carte projet
|
||||
|
||||
**Critères de succès** : L'utilisateur trouve la preuve de maîtrise (projet ou lien externe)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Page Compétences] --> B{Type de compétence?}
|
||||
B -->|Techno Dev| C[Liste technologies]
|
||||
B -->|Outil| D[Liste outils]
|
||||
C --> E[Clic sur technologie]
|
||||
E --> F[Voir projets associés]
|
||||
F --> G[Navigation vers projet]
|
||||
D --> H{Démontrable?}
|
||||
H -->|Oui| I[🔗 Lien preuve externe]
|
||||
H -->|Non| J[📝 Contexte d'utilisation]
|
||||
I --> K[Visite preuve: GitHub, Notion...]
|
||||
```
|
||||
|
||||
**Edge Cases & Gestion d'erreurs** :
|
||||
- Technologie sans projet associé : Masquer ou afficher avec mention "En apprentissage"
|
||||
- Lien externe cassé : Vérification régulière, fallback texte descriptif
|
||||
|
||||
## 4. Wireframes & Mockups
|
||||
|
||||
**Approche Design** : Wireframes low-fidelity en description textuelle. Le développement se fera directement en HTML/Tailwind CSS, ce document sert de référence.
|
||||
|
||||
### 4.1 Page d'Accueil
|
||||
|
||||
**Objectif** : Accroche immédiate + orientation vers les sections
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ NAVBAR (sticky) │
|
||||
│ [Logo] Accueil Projets Compétences À propos [CTA] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ HERO SECTION │
|
||||
│ [Photo ou Illustration] │
|
||||
│ Prénom NOM │
|
||||
│ Développeur Web Full-Stack │
|
||||
│ "Phrase d'accroche percutante" │
|
||||
│ [ Découvrir mes projets → ] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ SECTIONS RAPIDES │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ 📁 │ │ 🛠 │ │ 👤 │ │
|
||||
│ │ Projets │ │ Compétences │ │ Me Découvrir│ │
|
||||
│ │ [Voir →] │ │ [Voir →] │ │ [Voir →] │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ FOOTER LinkedIn GitHub Email © 2026 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Interactions** : Fade-in au chargement, hover sur cartes (élévation)
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Page Projets
|
||||
|
||||
**Objectif** : Présenter les projets vedettes + secondaires
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ NAVBAR │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Mes Projets │
|
||||
│ Découvrez les réalisations qui illustrent mon travail │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ PROJETS VEDETTES │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ [Thumbnail] │ │ [Thumbnail] │ │
|
||||
│ │ Titre Projet │ │ Titre Projet │ │
|
||||
│ │ [PHP] [JS] [CSS] │ │ [React] [Node] │ │
|
||||
│ └──────────────────┘ └──────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ AUTRES PROJETS │
|
||||
│ • Projet A — Description courte — [PHP, JS] │
|
||||
│ • Projet B — Description courte — [WordPress] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ FOOTER │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Interactions** : Hover cartes (ombre/élévation), badges cliquables
|
||||
|
||||
---
|
||||
|
||||
### 4.3 Page Projet Individuelle
|
||||
|
||||
**Objectif** : Détailler un projet avec preuves
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ NAVBAR │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Accueil > Projets > Nom du Projet (breadcrumb) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ TITRE DU PROJET │
|
||||
│ [PHP] [JavaScript] [Tailwind CSS] │
|
||||
│ ┌─────────────────────────────────────────┐ │
|
||||
│ │ [Image principale] │ │
|
||||
│ └─────────────────────────────────────────┘ │
|
||||
│ [ Voir le projet en ligne → ] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ CONTEXTE — Description du projet │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ SOLUTION TECHNIQUE — Technologies et argumentation │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ TRAVAIL D'ÉQUIPE — Rôle, organisation (si applicable) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ RÉALISATION — Durée + Galerie [img1] [img2] [img3] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 💬 TÉMOIGNAGE — "Citation..." — Nom, Rôle │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ [ ← Retour aux projets ] [ Me contacter → ] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ FOOTER │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Page Contact
|
||||
|
||||
**Objectif** : Conversion sans friction
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ NAVBAR │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Me Contacter │
|
||||
│ Une question, un projet ? Parlons-en ! │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────────────────────────────┐ │
|
||||
│ │ Nom * Prénom * │ │
|
||||
│ │ [_______________] [_______________] │ │
|
||||
│ │ Email * Entreprise │ │
|
||||
│ │ [_______________] [_______________] │ │
|
||||
│ │ Catégorie * [▼ Sélectionner...] │ │
|
||||
│ │ Objet * [_____________________] │ │
|
||||
│ │ Message * [_____________________] │ │
|
||||
│ │ [_____________________] │ │
|
||||
│ │ [ Envoyer le message ] │ │
|
||||
│ │ [Effacer le formulaire] │ │
|
||||
│ └─────────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Retrouvez-moi aussi sur : │
|
||||
│ [LinkedIn] [GitHub] [Email direct] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ FOOTER │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Interactions** : Validation temps réel, spinner à l'envoi, message de confirmation
|
||||
|
||||
## 5. Component Library / Design System
|
||||
|
||||
**Stratégie** : Utilisation de Tailwind CSS avec des classes utilitaires. Pas de design system externe. Les composants sont définis comme des patterns HTML/Tailwind réutilisables.
|
||||
|
||||
### 5.1 Bouton Principal (CTA)
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **Objectif** | Action principale (Contact, Voir projet) |
|
||||
| **Variantes** | Primary (rempli), Secondary (outline), Ghost (texte) |
|
||||
| **États** | Default, Hover, Focus, Disabled, Loading |
|
||||
|
||||
```
|
||||
Primary: [████████████] → Fond accent, texte blanc
|
||||
Secondary: [────────────] → Bordure accent, fond transparent
|
||||
Ghost: [ Texte → ] → Texte accent, pas de bordure
|
||||
```
|
||||
|
||||
### 5.2 Carte Projet
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **Objectif** | Aperçu cliquable d'un projet vedette |
|
||||
| **Variantes** | Standard (grille), Compact (liste secondaires) |
|
||||
| **États** | Default, Hover (élévation + ombre) |
|
||||
|
||||
**Structure** : Thumbnail (16:9, lazy loading) → Titre (H3) → Badges technologies (max 4)
|
||||
|
||||
### 5.3 Badge Technologie
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **Objectif** | Afficher une technologie/outil |
|
||||
| **Variantes** | Standard (petit), Large (page compétences) |
|
||||
| **États** | Default, Hover (si cliquable) |
|
||||
|
||||
Exemple : `[PHP] [JavaScript] [Tailwind CSS] [+3]`
|
||||
|
||||
### 5.4 Champ de Formulaire
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **Objectif** | Saisie utilisateur |
|
||||
| **Variantes** | Text, Email, Textarea, Select |
|
||||
| **États** | Default, Focus, Error, Disabled, Filled |
|
||||
|
||||
**Structure** : Label (*) → Input avec bordure → Message d'erreur inline
|
||||
|
||||
### 5.5 Navbar
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **Objectif** | Navigation globale persistante |
|
||||
| **Variantes** | Desktop (horizontal), Mobile (hamburger) |
|
||||
| **États** | Default, Scrolled (ombre), Menu ouvert |
|
||||
|
||||
**Structure** : Logo → Liens navigation (état actif) → Bouton CTA → Hamburger (mobile)
|
||||
|
||||
### 5.6 Section Témoignage
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **Objectif** | Afficher une citation client/employeur |
|
||||
| **Variantes** | Inline (page projet), Card (section dédiée) |
|
||||
|
||||
**Structure** : Guillemets → Citation (italique) → Auteur (nom, rôle, entreprise) → Photo (optionnel)
|
||||
|
||||
### 5.7 Breadcrumb
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **Objectif** | Indiquer la position dans la hiérarchie |
|
||||
| **Usage** | Pages projet individuelles uniquement |
|
||||
|
||||
Format : `Accueil > Projets > Nom du Projet` (derniers éléments cliquables sauf actuel)
|
||||
|
||||
### 5.8 Footer
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| **Objectif** | Informations secondaires et liens réseaux |
|
||||
|
||||
**Structure** : Icônes réseaux sociaux → Copyright → Mentions légales
|
||||
|
||||
## 6. Branding & Style Guide
|
||||
|
||||
### 6.1 Identité Visuelle
|
||||
|
||||
**Thème** : Sombre (Dark Mode)
|
||||
**Logo** : Imposé, disponible dans `assets/img/`
|
||||
**Style** : Moderne, clean, technique mais accessible
|
||||
|
||||
### 6.2 Palette de Couleurs (Thème Sombre)
|
||||
|
||||
| Type | Hex | Tailwind Config | Usage |
|
||||
|------|-----|-----------------|-------|
|
||||
| **Primary** | `#FA784F` | `primary` | CTA, liens, éléments d'accent |
|
||||
| **Primary Hover** | `#FB9570` | `primary-light` | Hover sur primary |
|
||||
| **Primary Dark** | `#E5623A` | `primary-dark` | Active/pressed |
|
||||
| **Background** | `#17171F` | `background` | Fond de page principal |
|
||||
| **Surface** | `#1E1E28` | `surface` | Cartes, navbar, composants |
|
||||
| **Surface Light** | `#2A2A36` | `surface-light` | Hover cartes, inputs focus |
|
||||
| **Border** | `#3A3A48` | `border` | Bordures, séparateurs |
|
||||
| **Text Primary** | `#F5F5F7` | `text-primary` | Titres, texte principal |
|
||||
| **Text Secondary** | `#A1A1AA` | `text-secondary` | Texte secondaire, labels |
|
||||
| **Text Muted** | `#71717A` | `text-muted` | Placeholders, désactivé |
|
||||
| **Success** | `#34D399` | `success` | Confirmations |
|
||||
| **Warning** | `#FBBF24` | `warning` | Avertissements |
|
||||
| **Error** | `#F87171` | `error` | Erreurs |
|
||||
| **Info** | `#60A5FA` | `info` | Informations |
|
||||
|
||||
### 6.3 Configuration Tailwind
|
||||
|
||||
```javascript
|
||||
// tailwind.config.js
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: '#FA784F',
|
||||
light: '#FB9570',
|
||||
dark: '#E5623A',
|
||||
},
|
||||
background: '#17171F',
|
||||
surface: {
|
||||
DEFAULT: '#1E1E28',
|
||||
light: '#2A2A36',
|
||||
},
|
||||
border: '#3A3A48',
|
||||
text: {
|
||||
primary: '#F5F5F7',
|
||||
secondary: '#A1A1AA',
|
||||
muted: '#71717A',
|
||||
},
|
||||
success: '#34D399',
|
||||
warning: '#FBBF24',
|
||||
error: '#F87171',
|
||||
info: '#60A5FA',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 Typographie
|
||||
|
||||
| Usage | Police | Fallback |
|
||||
|-------|--------|----------|
|
||||
| **Titres & Corps** | Inter | system-ui, sans-serif |
|
||||
| **Code** | JetBrains Mono | monospace |
|
||||
|
||||
**Échelle Typographique** :
|
||||
|
||||
| Élément | Taille | Poids | Line Height |
|
||||
|---------|--------|-------|-------------|
|
||||
| H1 | 2.5rem (40px) | 700 | 1.2 |
|
||||
| H2 | 2rem (32px) | 600 | 1.3 |
|
||||
| H3 | 1.5rem (24px) | 600 | 1.4 |
|
||||
| Body | 1rem (16px) | 400 | 1.6 |
|
||||
| Small | 0.875rem (14px) | 400 | 1.5 |
|
||||
|
||||
### 6.5 Iconographie
|
||||
|
||||
**Bibliothèque** : Heroicons (outline navigation, solid états actifs)
|
||||
**Tailles** : 24px (nav), 20px (inline), 16px (badges)
|
||||
**Couleur** : Hérite du texte parent
|
||||
|
||||
### 6.6 Espacements
|
||||
|
||||
**Système** : Base 4px, classes Tailwind standard
|
||||
|
||||
| Token | Valeur | Usage |
|
||||
|-------|--------|-------|
|
||||
| sm | 8px | Entre éléments proches |
|
||||
| md | 16px | Padding composants |
|
||||
| lg | 24px | Entre sections |
|
||||
| xl | 32px | Marges de section |
|
||||
| 2xl | 48px | Séparation majeure |
|
||||
|
||||
## 7. Exigences d'Accessibilité
|
||||
|
||||
### 7.1 Niveau de Conformité
|
||||
|
||||
**Standard** : WCAG 2.1 Niveau AA (bonnes pratiques, pas de certification formelle)
|
||||
|
||||
### 7.2 Contrastes de Couleurs
|
||||
|
||||
| Élément | Couleurs | Ratio | Conformité |
|
||||
|---------|----------|-------|------------|
|
||||
| Texte principal / fond | `#F5F5F7` / `#17171F` | 15.5:1 | AAA |
|
||||
| Texte secondaire / fond | `#A1A1AA` / `#17171F` | 6.8:1 | AA |
|
||||
| Primary / fond | `#FA784F` / `#17171F` | 5.2:1 | AA |
|
||||
| Texte / bouton primary | `#17171F` / `#FA784F` | 5.2:1 | AA |
|
||||
|
||||
### 7.3 Indicateurs de Focus
|
||||
|
||||
- **Style** : `outline: 2px solid #FA784F; outline-offset: 2px;`
|
||||
- **Visibilité** : Toujours visible, jamais supprimé
|
||||
- **Cohérence** : Identique sur tous les éléments interactifs
|
||||
|
||||
### 7.4 Navigation Clavier
|
||||
|
||||
| Touche | Action |
|
||||
|--------|--------|
|
||||
| `Tab` | Navigation séquentielle |
|
||||
| `Shift+Tab` | Navigation inverse |
|
||||
| `Enter` | Activation liens/boutons |
|
||||
| `Escape` | Fermeture menu mobile |
|
||||
|
||||
**Ordre** : `tabindex` naturel du DOM respecté
|
||||
|
||||
### 7.5 Support Lecteur d'Écran
|
||||
|
||||
- **Landmarks** : `<header>`, `<nav>`, `<main>`, `<footer>`
|
||||
- **Titres** : Hiérarchie H1 → H2 → H3 sans saut
|
||||
- **Labels** : Tous les inputs associés à un `<label>`
|
||||
- **États** : `aria-expanded`, `aria-invalid` utilisés
|
||||
|
||||
### 7.6 Textes Alternatifs
|
||||
|
||||
| Type | Traitement |
|
||||
|------|------------|
|
||||
| Décorative | `alt=""` |
|
||||
| Informative | `alt` descriptif |
|
||||
| Thumbnail projet | `alt="Aperçu du projet [Nom]"` |
|
||||
| Icône avec texte | `aria-hidden="true"` |
|
||||
| Icône seule | `aria-label` descriptif |
|
||||
|
||||
### 7.7 Cibles Tactiles
|
||||
|
||||
- **Taille minimum** : 44x44px
|
||||
- **Espacement** : 8px entre cibles adjacentes
|
||||
|
||||
### 7.8 Stratégie de Test
|
||||
|
||||
| Outil | Usage |
|
||||
|-------|-------|
|
||||
| Lighthouse | Score accessibilité > 90 |
|
||||
| axe DevTools | Erreurs ARIA |
|
||||
| Navigation clavier | Test manuel |
|
||||
| Contrast Checker | Validation ratios |
|
||||
|
||||
## 8. Stratégie Responsive
|
||||
|
||||
### 8.1 Breakpoints
|
||||
|
||||
| Breakpoint | Min Width | Max Width | Appareils |
|
||||
|------------|-----------|-----------|-----------|
|
||||
| **Mobile** | 0px | 639px | Smartphones |
|
||||
| **Tablet** | 640px | 1023px | Tablettes |
|
||||
| **Desktop** | 1024px | 1279px | Laptops |
|
||||
| **Wide** | 1280px | — | Grands écrans |
|
||||
|
||||
**Classes Tailwind** : `sm:` (640px), `md:` (768px), `lg:` (1024px), `xl:` (1280px)
|
||||
|
||||
### 8.2 Patterns d'Adaptation Layout
|
||||
|
||||
| Élément | Mobile | Tablet | Desktop |
|
||||
|---------|--------|--------|---------|
|
||||
| Conteneur | 100% - 16px padding | 100% - 24px padding | Max 1280px centré |
|
||||
| Grille projets | 1 colonne | 2 colonnes | 3 colonnes |
|
||||
| Cartes accueil | Empilées | 3 colonnes | 3 colonnes |
|
||||
| Formulaire | Champs empilés | Empilés | Nom/Prénom côte à côte |
|
||||
|
||||
### 8.3 Navigation Responsive
|
||||
|
||||
| Élément | Mobile | Desktop |
|
||||
|---------|--------|---------|
|
||||
| Menu | Hamburger → overlay | Liens horizontaux |
|
||||
| CTA Contact | Dans menu hamburger | Bouton navbar visible |
|
||||
| Logo | Centré/gauche | Gauche |
|
||||
|
||||
### 8.4 Adaptation des Interactions
|
||||
|
||||
| Comportement | Mobile | Desktop |
|
||||
|--------------|--------|---------|
|
||||
| Hover cartes | `:active` uniquement | Élévation + ombre |
|
||||
| Hover boutons | `:active` | Changement couleur |
|
||||
| Galerie | Swipe horizontal | Clic agrandissement |
|
||||
|
||||
### 8.5 Exemple Tailwind
|
||||
|
||||
```html
|
||||
<!-- Grille projets responsive -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- Cartes projet -->
|
||||
</div>
|
||||
```
|
||||
|
||||
## 9. Animations & Micro-interactions
|
||||
|
||||
### 9.1 Principes de Motion Design
|
||||
|
||||
1. **Subtilité** — Guider l'œil sans distraire du contenu
|
||||
2. **Intention** — Chaque animation a un but (feedback, orientation)
|
||||
3. **Performance** — Uniquement `transform` et `opacity` (GPU-accelerated)
|
||||
4. **Accessibilité** — Respect de `prefers-reduced-motion`
|
||||
|
||||
### 9.2 Animations Clés
|
||||
|
||||
| Animation | Élément | Durée | Easing |
|
||||
|-----------|---------|-------|--------|
|
||||
| Fade-in Hero | Titre, sous-titre, CTA | 600ms (staggered +100ms) | ease-out |
|
||||
| Hover Cartes | Cartes projet | 200ms | ease-in-out |
|
||||
| Hover Boutons | CTA | 150ms | ease |
|
||||
| Menu Mobile | Overlay | 300ms | ease-out |
|
||||
| Focus Input | Champs formulaire | 150ms | ease |
|
||||
| Spinner | Bouton envoi | 1000ms loop | linear |
|
||||
| Confirmation | Toast succès | 300ms | ease-out |
|
||||
|
||||
### 9.3 Exemples CSS
|
||||
|
||||
```css
|
||||
/* Hover carte projet */
|
||||
.project-card {
|
||||
transition: transform 200ms ease-in-out, box-shadow 200ms ease-in-out;
|
||||
}
|
||||
.project-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Hover bouton primary */
|
||||
.btn-primary {
|
||||
background-color: #FA784F;
|
||||
transition: background-color 150ms ease;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: #FB9570;
|
||||
}
|
||||
|
||||
/* Focus input */
|
||||
input:focus {
|
||||
border-color: #FA784F;
|
||||
box-shadow: 0 0 0 3px rgba(250, 120, 79, 0.2);
|
||||
}
|
||||
```
|
||||
|
||||
### 9.4 Accessibilité Motion
|
||||
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 10. Considérations de Performance
|
||||
|
||||
### 10.1 Objectifs
|
||||
|
||||
| Métrique | Objectif | Source PRD |
|
||||
|----------|----------|------------|
|
||||
| **Lighthouse Performance** | > 90 | NFR9 |
|
||||
| **First Contentful Paint (FCP)** | < 1.5s | NFR10 |
|
||||
| **Cumulative Layout Shift (CLS)** | < 0.1 | NFR11 |
|
||||
| **Time to Interactive (TTI)** | < 3s | NFR12 |
|
||||
| **CSS après purge** | < 50kb | Story 1.2 |
|
||||
|
||||
### 10.2 Stratégies Images
|
||||
|
||||
| Technique | Implémentation |
|
||||
|-----------|----------------|
|
||||
| Format WebP | `<picture>` avec fallback JPG/PNG |
|
||||
| Lazy loading | `loading="lazy"` sur toutes les images |
|
||||
| Dimensions | `width` et `height` explicites (évite CLS) |
|
||||
| Thumbnails | Max 400px, qualité 80% |
|
||||
|
||||
```html
|
||||
<picture>
|
||||
<source srcset="image.webp" type="image/webp">
|
||||
<img src="image.jpg" alt="..." width="400" height="225" loading="lazy">
|
||||
</picture>
|
||||
```
|
||||
|
||||
### 10.3 Stratégies CSS/JS
|
||||
|
||||
**CSS (Tailwind)** :
|
||||
- Purge automatique via config `content`
|
||||
- Minification avec `--minify`
|
||||
- Objectif : < 50kb
|
||||
|
||||
**JavaScript** :
|
||||
- Vanilla JS (pas de framework)
|
||||
- Attribut `defer` sur les scripts
|
||||
- Scripts en fin de `<body>`
|
||||
|
||||
### 10.4 Stratégies Fonts
|
||||
|
||||
```html
|
||||
<link rel="preload" href="fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
|
||||
```
|
||||
|
||||
- `font-display: swap` (évite FOIT)
|
||||
- Fallback : `system-ui, sans-serif`
|
||||
|
||||
### 10.5 Outils de Test
|
||||
|
||||
| Outil | Usage |
|
||||
|-------|-------|
|
||||
| Lighthouse | Audit complet (Chrome DevTools) |
|
||||
| PageSpeed Insights | Test conditions réelles |
|
||||
| WebPageTest | Analyse waterfall |
|
||||
|
||||
## 11. Prochaines Étapes
|
||||
|
||||
### 11.1 Actions Immédiates
|
||||
|
||||
1. **Revue avec les parties prenantes** — Valider cette spécification UI/UX
|
||||
2. **Passage à l'Architecte** — Transmettre ce document pour l'architecture technique
|
||||
3. **Préparation des assets** — Rassembler le logo, préparer les images des projets
|
||||
|
||||
### 11.2 Checklist de Validation Design
|
||||
|
||||
- [x] Objectifs UX et personas définis
|
||||
- [x] Architecture de l'information complète
|
||||
- [x] Flux utilisateurs documentés
|
||||
- [x] Wireframes des écrans clés
|
||||
- [x] Composants UI identifiés
|
||||
- [x] Palette de couleurs (thème sombre) validée
|
||||
- [x] Typographie définie
|
||||
- [x] Exigences d'accessibilité documentées
|
||||
- [x] Stratégie responsive définie
|
||||
- [x] Animations spécifiées
|
||||
- [x] Objectifs de performance établis
|
||||
|
||||
### 11.3 Handoff vers l'Architecte
|
||||
|
||||
| Élément | Référence |
|
||||
|---------|-----------|
|
||||
| Stack technique | PRD Section 4 |
|
||||
| Palette couleurs | Section 6.2 |
|
||||
| Config Tailwind | Section 6.3 |
|
||||
| Composants | Section 5 |
|
||||
| Performance | Section 10 |
|
||||
|
||||
### 11.4 Questions Ouvertes
|
||||
|
||||
| Question | Statut |
|
||||
|----------|--------|
|
||||
| Contenu exact de l'accroche Hero | À rédiger |
|
||||
| Sélection des 5-10 projets vedettes | À définir |
|
||||
| Photos/visuels pour "Me Découvrir" | À préparer |
|
||||
| Témoignages clients | À collecter |
|
||||
|
||||
---
|
||||
|
||||
*Document généré par Sally (UX Expert) - Méthode BMAD*
|
||||
|
||||
700
docs/prd.md
Normal file
700
docs/prd.md
Normal file
@@ -0,0 +1,700 @@
|
||||
# Portfolio Développeur Web - Product Requirements Document (PRD)
|
||||
|
||||
## 1. Objectifs et Contexte
|
||||
|
||||
### 1.1 Objectifs (Goals)
|
||||
|
||||
- **Convaincre recruteurs et clients potentiels** de mes compétences à travers des preuves tangibles et vérifiables
|
||||
- **Inspirer confiance** par l'authenticité totale et la démonstration plutôt que l'affirmation
|
||||
- **Faciliter la prise de contact** avec une expérience utilisateur sans friction
|
||||
- **Créer une connexion humaine** en montrant la personne derrière le développeur
|
||||
- **Démontrer la maîtrise technique** via des projets visitables et des explications techniques
|
||||
|
||||
### 1.2 Contexte (Background)
|
||||
|
||||
Ce projet consiste à reprendre de zéro un portfolio développeur web existant. L'objectif est de créer une vitrine professionnelle qui applique le principe fondamental **"Montrer plutôt que dire"** - chaque affirmation devant être soutenue par une preuve tangible et vérifiable.
|
||||
|
||||
Le portfolio cible deux audiences principales : les **recruteurs** cherchant un candidat fiable et compétent, et les **clients potentiels** souhaitant évaluer la capacité à livrer leurs projets. La stratégie repose sur l'authenticité vérifiable (projets en ligne, témoignages tiers, explications techniques prouvant la paternité du travail) et une expérience utilisateur fluide du premier clic jusqu'au formulaire de contact.
|
||||
|
||||
### 1.3 Journal des modifications (Change Log)
|
||||
|
||||
| Date | Version | Description | Auteur |
|
||||
|------|---------|-------------|--------|
|
||||
| 2026-01-22 | 0.1 | Création initiale basée sur le brainstorming | John (PM) |
|
||||
| 2026-01-22 | 1.0 | Version complète avec 5 epics, 24 stories, validation PM | John (PM) |
|
||||
|
||||
## 2. Exigences (Requirements)
|
||||
|
||||
### 2.1 Exigences Fonctionnelles (FR)
|
||||
|
||||
- **FR1**: Le portfolio doit afficher une page d'accueil avec une accroche claire et une navigation vers toutes les sections
|
||||
- **FR2**: Le portfolio doit présenter 5-10 projets vedettes avec des pages dédiées contenant : contexte, solution technique, réalisation, et lien vers le projet
|
||||
- **FR3**: Le portfolio doit afficher une liste de projets secondaires en format simplifié
|
||||
- **FR4**: Le portfolio doit présenter les compétences techniques liées aux projets correspondants (preuves vivantes)
|
||||
- **FR5**: Le portfolio doit permettre de démontrer les outils vérifiables via des liens externes (dépôts Git, pages Notion, etc.)
|
||||
- **FR6**: Le portfolio doit contenir une page "Me Découvrir" avec parcours, motivations et passions hors travail
|
||||
- **FR7**: Le portfolio doit afficher une section témoignages clients/employeurs
|
||||
- **FR8**: Le portfolio doit proposer un formulaire de contact avec les champs : Nom, Prénom, Email, Entreprise (optionnel), Catégorie (dropdown), Objet, Message
|
||||
- **FR9**: Le formulaire de contact doit persister les données si le visiteur quitte et revient
|
||||
- **FR10**: Le portfolio doit intégrer un bouton "Me contacter" visible en permanence dans la navbar
|
||||
- **FR11**: Le formulaire doit proposer 3 catégories de contact : Projet freelance, Proposition de poste, Autre
|
||||
- **FR12**: Le portfolio doit intégrer un captcha invisible (reCAPTCHA v3) pour la protection anti-spam
|
||||
|
||||
### 2.2 Exigences Non-Fonctionnelles (NFR)
|
||||
|
||||
- **NFR1**: Le site doit être développé en HTML/CSS/JS pur avec Tailwind CSS (pas de framework JS)
|
||||
- **NFR2**: Le site doit être responsive et s'adapter à tous les écrans (mobile, tablette, desktop)
|
||||
- **NFR3**: Le site doit être optimisé pour les performances (temps de chargement < 3s)
|
||||
- **NFR4**: Le site doit respecter les bonnes pratiques SEO de base (meta tags, structure sémantique)
|
||||
- **NFR5**: Le code doit être simple, maintenable et bien organisé
|
||||
- **NFR6**: Le design doit privilégier les animations subtiles sans distraire du contenu
|
||||
- **NFR7**: L'expérience utilisateur doit être fluide avec zéro friction du premier clic au contact
|
||||
- **NFR8**: Le ton rédactionnel doit être sympathique et authentique, ni trop formel ni trop familier
|
||||
- **NFR9**: Le score Lighthouse Performance doit être supérieur à 90
|
||||
- **NFR10**: Le First Contentful Paint (FCP) doit être inférieur à 1.5 secondes
|
||||
- **NFR11**: Le Cumulative Layout Shift (CLS) doit être inférieur à 0.1
|
||||
- **NFR12**: Le Time to Interactive (TTI) doit être inférieur à 3 secondes
|
||||
|
||||
### 2.3 Hors Scope MVP
|
||||
|
||||
Les éléments suivants sont explicitement exclus du MVP :
|
||||
|
||||
| Feature | Raison |
|
||||
|---------|--------|
|
||||
| Blog / Articles | Complexité ajoutée, pas essentiel pour la conversion |
|
||||
| Multi-langue (i18n) | Portfolio FR uniquement pour le MVP |
|
||||
| Mode sombre (dark mode) | Nice-to-have, peut être ajouté plus tard |
|
||||
| Analytics avancés | Google Analytics basique suffit, pas de dashboard custom |
|
||||
| CMS avec interface admin | Le fichier JSON est suffisant pour 5-10 projets |
|
||||
| Système de commentaires | Pas pertinent pour un portfolio |
|
||||
| Newsletter / Inscription | Le formulaire de contact suffit |
|
||||
|
||||
## 3. Objectifs de Design UI/UX
|
||||
|
||||
### 3.1 Vision UX Globale
|
||||
|
||||
Un portfolio qui incarne le principe **"Le design au service du message"**. L'interface doit être soignée et professionnelle tout en restant au second plan par rapport au contenu. Le visiteur doit pouvoir naviguer intuitivement et trouver rapidement les informations recherchées, que ce soit un projet spécifique, des compétences ou le formulaire de contact.
|
||||
|
||||
### 3.2 Paradigmes d'Interaction Clés
|
||||
|
||||
- **Navigation claire** : Menu principal toujours accessible avec CTA "Me contacter" visible
|
||||
- **Scroll fluide** : Pages aérées favorisant la lecture verticale
|
||||
- **Animations subtiles** : Micro-interactions qui guident l'œil sans distraire
|
||||
- **Liens contextuels** : Compétences liées aux projets, outils vers preuves externes
|
||||
- **Progressive disclosure** : Projets vedettes en détail, projets secondaires en liste
|
||||
|
||||
### 3.3 Écrans et Vues Principales
|
||||
|
||||
| Écran | Description |
|
||||
|-------|-------------|
|
||||
| Page d'accueil | Accroche + navigation vers sections principales |
|
||||
| Liste des projets | Grille/liste des projets vedettes avec aperçu visuel |
|
||||
| Page projet (x5-10) | Contexte → Solution → Réalisation → Lien/Démo |
|
||||
| Page Compétences | Technos liées aux projets + outils démontrables |
|
||||
| Page Me Découvrir | Parcours, motivations, passions (ton personnel) |
|
||||
| Section Témoignages | Avis clients/employeurs avec attribution |
|
||||
| Page Contact | Formulaire principal + liens réseaux secondaires |
|
||||
|
||||
### 3.4 Accessibilité
|
||||
|
||||
**Niveau : Bonnes pratiques de base** (pas de certification WCAG formelle)
|
||||
|
||||
- Contrastes suffisants pour la lisibilité
|
||||
- Navigation au clavier fonctionnelle
|
||||
- Textes alternatifs pour les images
|
||||
- Structure sémantique HTML5
|
||||
|
||||
### 3.5 Branding
|
||||
|
||||
- Pas de charte graphique imposée - liberté créative
|
||||
- Style moderne, clean, technique mais accessible
|
||||
- Palette de couleurs : tons neutres + couleur d'accent
|
||||
- Typographie lisible et professionnelle
|
||||
|
||||
### 3.6 Plateformes Cibles
|
||||
|
||||
**Web Responsive** avec approche mobile-first :
|
||||
- Mobile (prioritaire)
|
||||
- Tablette
|
||||
- Desktop (écrans larges)
|
||||
|
||||
## 4. Hypothèses Techniques
|
||||
|
||||
### 4.1 Structure du Repository
|
||||
|
||||
**Monorepo** - Un seul dépôt Git contenant l'ensemble du projet.
|
||||
|
||||
### 4.2 Architecture de Service
|
||||
|
||||
**Site PHP léger (Multi-Page)** - Architecture simple sans framework.
|
||||
|
||||
| Composant | Technologie | Justification |
|
||||
|-----------|-------------|---------------|
|
||||
| **Backend** | PHP natif | Formulaire de contact + génération pages projets |
|
||||
| **Frontend** | HTML5 + Tailwind CSS + JS vanilla | Simplicité, performance |
|
||||
| **Gestion projets** | Fichier JSON + templates PHP | Maintenabilité sans CMS lourd |
|
||||
| **Formulaire** | PHP personnalisé | Contrôle total, pas de dépendance externe |
|
||||
| **Captcha** | Google reCAPTCHA v3 | Protection anti-spam invisible |
|
||||
| **Persistance formulaire** | localStorage | Côté navigateur, simple |
|
||||
|
||||
### 4.3 Stack Technique Complet
|
||||
|
||||
| Catégorie | Choix | Rationale |
|
||||
|-----------|-------|-----------|
|
||||
| **Markup** | HTML5 sémantique + PHP | SEO + accessibilité + dynamisme |
|
||||
| **Styling** | Tailwind CSS (CLI build) | Utility-first, purge CSS pour performance |
|
||||
| **Scripting** | JavaScript ES6+ vanilla | Pas de framework, simplicité |
|
||||
| **Icons** | SVG inline ou sprite | Performance, scalabilité |
|
||||
| **Hébergement** | Serveur personnel (PHP) | Infrastructure existante |
|
||||
| **Build** | Tailwind CLI + PostCSS | CSS optimisé et minifié |
|
||||
| **Versioning** | Git | Standard industrie |
|
||||
|
||||
### 4.4 Structure des Fichiers (Proposée)
|
||||
|
||||
```
|
||||
/portfolio
|
||||
├── index.php # Page d'accueil
|
||||
├── projects.php # Liste des projets
|
||||
├── project.php # Template page projet unique
|
||||
├── skills.php # Page compétences
|
||||
├── about.php # Page "Me Découvrir"
|
||||
├── contact.php # Page contact + traitement formulaire
|
||||
├── data/
|
||||
│ └── projects.json # Données des projets (mini-CMS)
|
||||
├── templates/
|
||||
│ ├── header.php # En-tête commun
|
||||
│ ├── footer.php # Pied de page commun
|
||||
│ ├── navbar.php # Navigation
|
||||
│ └── project-card.php # Carte projet réutilisable
|
||||
├── assets/
|
||||
│ ├── css/
|
||||
│ │ ├── input.css # Source Tailwind
|
||||
│ │ └── output.css # CSS compilé
|
||||
│ ├── js/
|
||||
│ │ └── main.js # Scripts (localStorage, animations)
|
||||
│ └── img/
|
||||
│ └── projects/ # Images des projets
|
||||
└── tailwind.config.js # Configuration Tailwind
|
||||
```
|
||||
|
||||
### 4.5 Exigences de Tests
|
||||
|
||||
| Type | Outil/Méthode |
|
||||
|------|---------------|
|
||||
| **Validation HTML** | W3C Validator |
|
||||
| **Tests responsive** | DevTools navigateur + appareils réels |
|
||||
| **Tests performance** | Lighthouse, PageSpeed Insights |
|
||||
| **Tests formulaire** | Tests manuels envoi/réception |
|
||||
| **Tests sécurité** | Vérification XSS, CSRF, injection |
|
||||
|
||||
### 4.6 Hypothèses Additionnelles
|
||||
|
||||
- **Pas de base de données** : Les données projets sont dans un fichier JSON
|
||||
- **Images optimisées** : Format WebP avec fallback, lazy loading
|
||||
- **Fonts** : Google Fonts ou auto-hébergées
|
||||
- **Email** : Envoi via fonction mail() PHP ou SMTP configuré sur le serveur
|
||||
- **SSL/HTTPS** : Certificat Let's Encrypt sur le serveur personnel
|
||||
|
||||
### 4.7 Stratégie de Déploiement
|
||||
|
||||
| Aspect | Détail |
|
||||
|--------|--------|
|
||||
| **Méthode** | Déploiement manuel via FTP/SFTP ou Git pull sur le serveur |
|
||||
| **Environnements** | Local (développement) → Production (serveur personnel) |
|
||||
| **Build avant déploiement** | `npm run build` pour générer le CSS Tailwind optimisé |
|
||||
| **Fichiers à déployer** | Tous sauf `node_modules/`, `package.json`, `package-lock.json`, `tailwind.config.js` |
|
||||
| **Rollback** | Conserver une copie de la version précédente sur le serveur |
|
||||
| **Vérification post-déploiement** | Test manuel des pages principales + formulaire de contact |
|
||||
|
||||
## 5. Liste des Epics
|
||||
|
||||
| Epic | Titre | Objectif |
|
||||
|------|-------|----------|
|
||||
| **Epic 1** | Fondations & Infrastructure | Établir la structure du projet, le build Tailwind, et les templates PHP de base |
|
||||
| **Epic 2** | Navigation & Page d'Accueil | Créer la navbar avec CTA, le header/footer, et la page d'accueil avec accroche |
|
||||
| **Epic 3** | Système de Projets | Implémenter le mini-CMS JSON, la liste des projets et les pages projet individuelles |
|
||||
| **Epic 4** | Pages de Contenu | Développer les pages Compétences, Me Découvrir, et Témoignages |
|
||||
| **Epic 5** | Formulaire de Contact | Créer le formulaire complet avec validation, localStorage, reCAPTCHA et envoi PHP |
|
||||
|
||||
## 6. Détail des Epics
|
||||
|
||||
### Epic 1 : Fondations & Infrastructure
|
||||
|
||||
**Objectif** : Établir la base technique du projet en configurant l'environnement de développement, le build Tailwind CSS, et la structure de fichiers PHP. À la fin de cet epic, une page "canary" simple sera accessible sur le serveur, validant que l'infrastructure fonctionne correctement.
|
||||
|
||||
#### Story 1.1 : Initialisation du projet et structure de fichiers
|
||||
|
||||
> **En tant que** développeur,
|
||||
> **Je veux** initialiser la structure du projet avec les dossiers et fichiers de base,
|
||||
> **Afin de** disposer d'une organisation claire et maintenable dès le départ.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. La structure de dossiers est créée : `templates/`, `assets/css/`, `assets/js/`, `assets/img/projects/`, `data/`
|
||||
2. Un fichier `index.php` existe à la racine avec un contenu minimal ("Hello World")
|
||||
3. Un fichier `.gitignore` est configuré (node_modules, .env, etc.)
|
||||
4. Le projet est initialisé avec Git et un premier commit est effectué
|
||||
|
||||
#### Story 1.2 : Configuration de Tailwind CSS avec CLI
|
||||
|
||||
> **En tant que** développeur,
|
||||
> **Je veux** configurer Tailwind CSS avec le CLI et le build optimisé,
|
||||
> **Afin de** bénéficier d'un CSS performant avec purge automatique.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. Node.js et npm sont utilisés uniquement pour le build (pas en production)
|
||||
2. `tailwind.config.js` est créé avec les chemins des fichiers PHP à scanner
|
||||
3. Le fichier `assets/css/input.css` contient les directives Tailwind (@tailwind base, etc.)
|
||||
4. La commande `npm run build` génère `assets/css/output.css` minifié
|
||||
5. Une commande `npm run watch` permet le développement en temps réel
|
||||
6. Le fichier CSS généré fait moins de 50kb après purge
|
||||
|
||||
#### Story 1.3 : Templates PHP de base (header/footer)
|
||||
|
||||
> **En tant que** développeur,
|
||||
> **Je veux** créer les templates PHP réutilisables pour le header et le footer,
|
||||
> **Afin de** ne pas dupliquer le code HTML commun sur chaque page.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. `templates/header.php` contient le doctype, head, meta tags SEO de base, et lien vers le CSS
|
||||
2. `templates/footer.php` contient la fermeture body, les scripts JS, et le copyright
|
||||
3. Le header inclut les balises meta viewport pour le responsive
|
||||
4. Le header permet de passer un titre de page dynamique via une variable PHP
|
||||
5. Les templates sont inclus dans `index.php` et la page s'affiche correctement
|
||||
|
||||
#### Story 1.4 : Page canary et validation du déploiement
|
||||
|
||||
> **En tant que** développeur,
|
||||
> **Je veux** déployer une page "canary" minimale sur le serveur,
|
||||
> **Afin de** valider que l'infrastructure PHP et le CSS fonctionnent en production.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. La page index.php affiche un message de test stylé avec Tailwind (ex: titre centré, couleur d'accent)
|
||||
2. La page est responsive (vérifiable sur mobile)
|
||||
3. Le déploiement sur le serveur personnel fonctionne
|
||||
4. Le site est accessible via HTTPS
|
||||
5. Le temps de chargement est inférieur à 2 secondes (Lighthouse)
|
||||
|
||||
### Epic 2 : Navigation & Page d'Accueil
|
||||
|
||||
**Objectif** : Créer le système de navigation global du portfolio avec le bouton CTA "Me contacter" toujours visible, ainsi que la page d'accueil avec une accroche percutante. À la fin de cet epic, les visiteurs pourront naviguer entre les sections et comprendre immédiatement l'identité du développeur.
|
||||
|
||||
#### Story 2.1 : Navbar responsive avec menu mobile
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** disposer d'une navigation claire et accessible sur tous les appareils,
|
||||
> **Afin de** trouver facilement les sections du portfolio.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. `templates/navbar.php` contient le menu de navigation avec les liens : Accueil, Projets, Compétences, Me Découvrir, Contact
|
||||
2. La navbar est fixe en haut de page (sticky) et reste visible au scroll
|
||||
3. Sur mobile, un menu "hamburger" affiche/masque les liens en JavaScript vanilla
|
||||
4. Le lien de la page active est visuellement distingué (couleur/soulignement)
|
||||
5. La navbar est responsive et s'adapte aux 3 breakpoints (mobile, tablette, desktop)
|
||||
|
||||
#### Story 2.2 : Bouton CTA "Me contacter" dans la navbar
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** voir un bouton "Me contacter" visible en permanence,
|
||||
> **Afin de** pouvoir initier un contact à tout moment sans chercher.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. Le bouton "Me contacter" est stylé différemment des autres liens (bouton rempli, couleur d'accent)
|
||||
2. Le bouton est visible sur desktop ET sur mobile (même dans le menu hamburger)
|
||||
3. Le bouton redirige vers la page contact.php
|
||||
4. Le bouton a un état hover/focus visible
|
||||
5. Le bouton respecte l'accessibilité (focusable, contraste suffisant)
|
||||
|
||||
#### Story 2.3 : Page d'accueil avec accroche
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** voir une page d'accueil avec une accroche claire et engageante,
|
||||
> **Afin de** comprendre immédiatement qui est le développeur et ce qu'il propose.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. La page d'accueil affiche une section "hero" avec : nom/prénom, titre (développeur web), et une phrase d'accroche
|
||||
2. Un bouton secondaire CTA invite à découvrir les projets
|
||||
3. Le design est aéré et la typographie met en valeur l'accroche
|
||||
4. La page inclut la navbar et le footer
|
||||
5. Le contenu est centré et responsive
|
||||
6. Les animations sont subtiles (fade-in au chargement, optionnel)
|
||||
|
||||
#### Story 2.4 : Sections de navigation rapide sur l'accueil
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** voir un aperçu des sections principales sur la page d'accueil,
|
||||
> **Afin de** naviguer rapidement vers ce qui m'intéresse.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. Sous le hero, des cartes/blocs présentent les sections : Projets, Compétences, Me Découvrir
|
||||
2. Chaque bloc a un titre, une courte description, et un lien vers la page correspondante
|
||||
3. Les blocs sont disposés en grille responsive (1 colonne mobile, 3 colonnes desktop)
|
||||
4. Les blocs ont un effet hover subtil
|
||||
5. L'ensemble reste cohérent avec le design global
|
||||
|
||||
### Epic 3 : Système de Projets
|
||||
|
||||
**Objectif** : Implémenter le cœur du portfolio - le système de gestion des projets basé sur JSON avec un router PHP pour des URLs propres. Cela inclut la structure de données, la page listant tous les projets vedettes, les pages individuelles pour chaque projet, et la gestion des projets secondaires. À la fin de cet epic, le principe "Montrer plutôt que dire" sera pleinement opérationnel.
|
||||
|
||||
#### Story 3.1 : Structure de données JSON pour les projets
|
||||
|
||||
> **En tant que** développeur,
|
||||
> **Je veux** définir et créer le fichier JSON contenant les données des projets,
|
||||
> **Afin de** centraliser les informations et faciliter la maintenance.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. Le fichier `data/projects.json` est créé avec la structure définie
|
||||
2. La structure supporte : id, title, slug, category (vedette/secondaire), thumbnail, url, technologies[], context, solution, teamwork, duration, testimonial, screenshots[]
|
||||
3. Au moins 2 projets de test sont ajoutés pour valider la structure
|
||||
4. Une fonction PHP `getProjects()` lit et décode le JSON
|
||||
5. La fonction gère les erreurs (fichier manquant, JSON invalide)
|
||||
|
||||
#### Story 3.2 : Router PHP et URLs propres
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** des URLs lisibles et propres pour accéder aux projets,
|
||||
> **Afin de** comprendre le contenu de la page avant même de cliquer et améliorer le SEO.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. Un fichier `.htaccess` redirige toutes les requêtes vers `index.php` (front controller)
|
||||
2. Un router PHP simple parse l'URL et route vers le bon fichier/action
|
||||
3. Les URLs des projets sont au format `/projet/{slug}` (ex: `/projet/site-ecommerce-xyz`)
|
||||
4. Les autres pages gardent des URLs simples : `/projets`, `/competences`, `/a-propos`, `/contact`
|
||||
5. Une route 404 personnalisée gère les URLs inconnues
|
||||
6. Le router est léger (<50 lignes de code) et sans dépendance externe
|
||||
|
||||
**Structure d'URLs :**
|
||||
|
||||
| URL | Action |
|
||||
|-----|--------|
|
||||
| `/` | Page d'accueil |
|
||||
| `/projets` | Liste des projets |
|
||||
| `/projet/{slug}` | Page projet individuelle |
|
||||
| `/competences` | Page compétences |
|
||||
| `/a-propos` | Page Me Découvrir |
|
||||
| `/contact` | Page contact |
|
||||
|
||||
#### Story 3.3 : Page liste des projets vedettes
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** voir une liste visuelle de tous les projets vedettes,
|
||||
> **Afin de** parcourir rapidement le portfolio et choisir ce qui m'intéresse.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. `/projets` affiche tous les projets où category = "vedette"
|
||||
2. Chaque projet est affiché sous forme de carte avec : thumbnail, titre, technologies (badges)
|
||||
3. Les cartes sont cliquables et redirigent vers `/projet/{slug}`
|
||||
4. La grille est responsive (1 col mobile, 2 cols tablette, 3 cols desktop)
|
||||
5. Les cartes ont un effet hover (légère élévation/ombre)
|
||||
6. Le template `templates/project-card.php` est réutilisable
|
||||
|
||||
#### Story 3.4 : Page projet individuelle
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** consulter une page dédiée pour chaque projet vedette,
|
||||
> **Afin de** comprendre le contexte, la solution technique et voir le résultat.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. L'URL `/projet/{slug}` affiche le projet correspondant au slug
|
||||
2. Le slug est récupéré depuis le router (pas depuis $_GET)
|
||||
3. La page affiche les sections : Contexte, Solution technique, Travail d'équipe (si applicable), Durée, Témoignage (si disponible)
|
||||
4. Un bouton/lien permet de visiter le projet en ligne (ou affiche "Non disponible")
|
||||
5. Les technologies sont affichées sous forme de badges
|
||||
6. Des captures d'écran sont affichées si disponibles (galerie simple)
|
||||
7. Un lien "Retour aux projets" permet de revenir à la liste
|
||||
8. Si le slug n'existe pas, la page 404 est affichée
|
||||
|
||||
#### Story 3.5 : Liste des projets secondaires
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** voir une liste simplifiée des projets secondaires,
|
||||
> **Afin de** découvrir l'étendue du travail sans page dédiée pour chaque.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. Sur `/projets`, une section "Autres projets" liste les projets où category = "secondaire"
|
||||
2. Chaque projet secondaire affiche : titre, courte description (1 ligne), technologies
|
||||
3. Le format est compact (liste ou petites cartes)
|
||||
4. Les projets secondaires peuvent avoir un lien externe direct (optionnel)
|
||||
5. La section est visuellement distincte des projets vedettes (séparateur, titre de section)
|
||||
|
||||
#### Story 3.6 : Optimisation des images projets
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** que les images des projets se chargent rapidement,
|
||||
> **Afin de** ne pas attendre et avoir une expérience fluide.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. Les images sont stockées dans `assets/img/projects/`
|
||||
2. Le lazy loading est activé sur toutes les images (attribut `loading="lazy"`)
|
||||
3. Les images ont des dimensions explicites (width/height) pour éviter le layout shift
|
||||
4. Le format WebP est utilisé avec fallback JPG/PNG via `<picture>`
|
||||
5. Les thumbnails sont redimensionnés (max 400px de large)
|
||||
6. Le score Lighthouse "Images" est vert (>90)
|
||||
|
||||
### Epic 4 : Pages de Contenu
|
||||
|
||||
**Objectif** : Développer les pages de contenu qui complètent le portfolio - Compétences, Me Découvrir, et Témoignages. Ces pages permettent au visiteur de mieux connaître le développeur, ses outils, et ce que d'autres pensent de son travail. À la fin de cet epic, toutes les pages statiques seront fonctionnelles.
|
||||
|
||||
#### Story 4.1 : Page Compétences - Technologies liées aux projets
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** voir les compétences techniques du développeur liées aux projets réalisés,
|
||||
> **Afin de** vérifier qu'il maîtrise les technologies dont j'ai besoin.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. `/competences` affiche une liste des technologies de développement (HTML, CSS, JS, PHP, etc.)
|
||||
2. Chaque technologie est liée aux projets qui l'utilisent (liens cliquables)
|
||||
3. Les technologies sont groupées par catégorie (Frontend, Backend, Outils, etc.)
|
||||
4. Un indicateur visuel montre le nombre de projets utilisant chaque technologie
|
||||
5. Le design utilise des badges/tags cohérents avec les pages projets
|
||||
|
||||
#### Story 4.2 : Page Compétences - Outils démontrables
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** voir les outils maîtrisés avec des preuves concrètes,
|
||||
> **Afin de** vérifier les compétences au-delà des simples affirmations.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. Une section "Outils démontrables" liste les outils avec liens de preuve (Git → GitHub, Notion → page publique, etc.)
|
||||
2. Chaque outil a : nom, icône/logo, lien vers la preuve externe
|
||||
3. Une section "Autres outils" liste les outils non démontrables avec contexte d'utilisation
|
||||
4. Le design distingue clairement les deux types d'outils
|
||||
5. Les liens externes s'ouvrent dans un nouvel onglet
|
||||
|
||||
#### Story 4.3 : Page Me Découvrir - Parcours et motivations
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** en savoir plus sur le développeur en tant que personne,
|
||||
> **Afin de** créer une connexion humaine et évaluer la compatibilité.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. `/a-propos` affiche les sections : Qui je suis, Mon parcours, Pourquoi ce métier
|
||||
2. Le ton est sympathique et authentique (pas trop formel, pas trop familier)
|
||||
3. Le texte est aéré avec des paragraphes courts
|
||||
4. Une photo professionnelle ou illustration est présente (optionnel mais recommandé)
|
||||
5. La localisation est mentionnée de façon générale (grande ville, pas adresse précise)
|
||||
|
||||
#### Story 4.4 : Page Me Découvrir - Passions et hobbies
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** découvrir les passions du développeur en dehors du code,
|
||||
> **Afin de** voir la personne au-delà du professionnel.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. Une section "En dehors du code" présente les hobbies et passions
|
||||
2. Des preuves visuelles sont incluses si possible (photos d'événements, créations, etc.)
|
||||
3. Le contenu reste professionnel (pas d'informations trop personnelles)
|
||||
4. Les projets personnels sont mentionnés comme preuve de passion implicite
|
||||
5. Le design intègre harmonieusement texte et visuels
|
||||
|
||||
#### Story 4.5 : Section Témoignages (JSON dynamique)
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** lire des témoignages de clients ou employeurs,
|
||||
> **Afin de** avoir une preuve sociale de la qualité du travail.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. Le fichier `data/testimonials.json` stocke tous les témoignages
|
||||
2. La structure JSON supporte : id, quote, author_name, author_role, author_company, author_photo, project_slug (optionnel), date, featured (booléen)
|
||||
3. Une fonction PHP `getTestimonials()` lit et décode le JSON
|
||||
4. La section témoignages affiche dynamiquement les entrées du JSON
|
||||
5. Chaque témoignage affiche : citation, nom, rôle/entreprise, photo (si disponible)
|
||||
6. Si un témoignage est lié à un projet (`project_slug`), un lien vers le projet est affiché
|
||||
7. Les témoignages `featured: true` peuvent être affichés sur la page d'accueil
|
||||
8. Si le JSON est vide ou le fichier absent, la section affiche "Témoignages à venir" ou est masquée
|
||||
9. Le design utilise des guillemets ou un style "citation" reconnaissable
|
||||
|
||||
**Structure `data/testimonials.json` :**
|
||||
```json
|
||||
{
|
||||
"testimonials": [
|
||||
{
|
||||
"id": 1,
|
||||
"quote": "Excellent travail, livraison dans les délais...",
|
||||
"author_name": "Marie Dupont",
|
||||
"author_role": "Directrice Marketing",
|
||||
"author_company": "Entreprise XYZ",
|
||||
"author_photo": "marie-dupont.webp",
|
||||
"project_slug": "site-ecommerce-xyz",
|
||||
"date": "2025-06-15",
|
||||
"featured": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Epic 5 : Formulaire de Contact
|
||||
|
||||
**Objectif** : Créer le formulaire de contact complet avec toutes les fonctionnalités définies : validation côté client et serveur, persistance localStorage, protection reCAPTCHA v3, et envoi d'email via PHP. À la fin de cet epic, les visiteurs pourront envoyer des messages de manière fluide et sécurisée.
|
||||
|
||||
#### Story 5.1 : Structure du formulaire et validation HTML5
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** un formulaire de contact clair avec des champs bien identifiés,
|
||||
> **Afin de** savoir exactement quelles informations fournir.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
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
|
||||
|
||||
#### Story 5.2 : Validation JavaScript côté client
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** être informé immédiatement si je fais une erreur de saisie,
|
||||
> **Afin de** corriger avant d'envoyer et éviter les allers-retours.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. La validation JavaScript s'exécute à la soumission ET à la perte de focus (blur)
|
||||
2. Les messages d'erreur sont affichés sous chaque champ concerné
|
||||
3. Les champs en erreur sont visuellement distingués (bordure rouge, icône)
|
||||
4. Le message d'erreur est clair et indique comment corriger
|
||||
5. Le bouton d'envoi est désactivé tant que le formulaire contient des erreurs
|
||||
6. La validation est en JavaScript vanilla (pas de bibliothèque)
|
||||
|
||||
#### Story 5.3 : Persistance des données avec localStorage
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** que mes données soient sauvegardées si je quitte la page,
|
||||
> **Afin de** ne pas tout ressaisir si je reviens plus tard.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. Chaque modification d'un champ sauvegarde automatiquement dans localStorage
|
||||
2. Au chargement de la page, les champs sont pré-remplis avec les données sauvegardées
|
||||
3. Le localStorage est vidé après un envoi réussi du formulaire
|
||||
4. Un bouton "Effacer le formulaire" permet de réinitialiser manuellement
|
||||
5. Les données sensibles (si ajoutées plus tard) ne sont PAS stockées
|
||||
6. Le stockage utilise une clé unique (ex: `portfolio_contact_form`)
|
||||
|
||||
#### Story 5.4 : Intégration reCAPTCHA v3
|
||||
|
||||
> **En tant que** propriétaire du site,
|
||||
> **Je veux** une protection anti-spam invisible,
|
||||
> **Afin de** ne pas recevoir de spam sans pénaliser l'expérience utilisateur.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. reCAPTCHA v3 est intégré (invisible, pas de case à cocher)
|
||||
2. Le script reCAPTCHA est chargé depuis Google
|
||||
3. Un token est généré à la soumission du formulaire
|
||||
4. Le token est envoyé avec les données du formulaire au backend PHP
|
||||
5. Les clés API (site key) sont configurables (fichier de config ou .env)
|
||||
6. Si reCAPTCHA échoue à charger, le formulaire reste utilisable (dégradation gracieuse)
|
||||
|
||||
#### Story 5.5 : Traitement PHP et envoi d'email
|
||||
|
||||
> **En tant que** propriétaire du site,
|
||||
> **Je veux** recevoir les messages par email de manière sécurisée,
|
||||
> **Afin de** pouvoir répondre aux visiteurs.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. Le backend PHP valide à nouveau tous les champs (ne jamais faire confiance au client)
|
||||
2. Le token reCAPTCHA est vérifié via l'API Google (score > 0.5)
|
||||
3. Les données sont nettoyées (htmlspecialchars, trim) contre XSS
|
||||
4. Un token CSRF est vérifié pour éviter les attaques cross-site
|
||||
5. L'email est envoyé via `mail()` PHP ou SMTP configuré
|
||||
6. L'email contient : tous les champs du formulaire, catégorie, date/heure, IP (optionnel)
|
||||
7. En cas de succès, une réponse JSON `{"success": true}` est renvoyée
|
||||
8. En cas d'erreur, une réponse JSON avec le message d'erreur est renvoyée
|
||||
|
||||
#### Story 5.6 : Feedback utilisateur (succès/erreur)
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** savoir clairement si mon message a été envoyé,
|
||||
> **Afin de** ne pas douter et éviter les envois multiples.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
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
|
||||
|
||||
#### Story 5.7 : Liens de contact secondaires
|
||||
|
||||
> **En tant que** visiteur,
|
||||
> **Je veux** avoir des alternatives au formulaire pour contacter le développeur,
|
||||
> **Afin de** choisir le canal qui me convient.
|
||||
|
||||
**Critères d'acceptation :**
|
||||
1. Sous le formulaire, une section affiche les liens secondaires : LinkedIn, GitHub, email direct (mailto)
|
||||
2. Les liens sont affichés avec leurs icônes respectives
|
||||
3. Les liens s'ouvrent dans un nouvel onglet (sauf mailto)
|
||||
4. La section est visuellement distincte mais cohérente avec le formulaire
|
||||
5. L'adresse email est protégée contre le scraping (encodage ou JS)
|
||||
|
||||
## 7. Rapport de Validation PM
|
||||
|
||||
### Résumé
|
||||
|
||||
| Métrique | Valeur |
|
||||
|----------|--------|
|
||||
| **Complétude du PRD** | 95% |
|
||||
| **Scope MVP** | Approprié |
|
||||
| **Prêt pour l'architecture** | ✅ Oui |
|
||||
|
||||
### Statut par Catégorie
|
||||
|
||||
| Catégorie | Statut |
|
||||
|-----------|--------|
|
||||
| Définition du problème & Contexte | ✅ PASS |
|
||||
| Scope MVP | ✅ PASS |
|
||||
| Exigences UX | ✅ PASS |
|
||||
| Exigences fonctionnelles | ✅ PASS |
|
||||
| Exigences non-fonctionnelles | ✅ PASS |
|
||||
| Structure Epic/Story | ✅ PASS |
|
||||
| Guidance technique | ✅ PASS |
|
||||
| Exigences cross-fonctionnelles | ✅ PASS |
|
||||
| Clarté & Communication | ✅ PASS |
|
||||
|
||||
### Décision
|
||||
|
||||
**✅ READY FOR ARCHITECT** - Le PRD est complet et prêt pour la phase d'architecture.
|
||||
|
||||
## 8. Prochaines Étapes
|
||||
|
||||
### 8.1 Prompt UX Expert
|
||||
|
||||
```
|
||||
Bonjour, je suis le Product Manager et j'ai finalisé le PRD pour un portfolio développeur web.
|
||||
|
||||
Le document se trouve dans docs/prd.md.
|
||||
|
||||
Objectif principal : Créer un design qui incarne le principe "Montrer plutôt que dire" avec une expérience utilisateur fluide et sans friction.
|
||||
|
||||
Points clés à considérer :
|
||||
- Navigation claire avec CTA "Me contacter" toujours visible
|
||||
- Pages projets avec structure : Contexte → Solution → Réalisation
|
||||
- Formulaire de contact optimisé pour la conversion
|
||||
- Design responsive mobile-first
|
||||
- Animations subtiles au service du contenu
|
||||
|
||||
Merci de proposer des wireframes et recommandations UX basés sur ce PRD.
|
||||
```
|
||||
|
||||
### 8.2 Prompt Architecte
|
||||
|
||||
```
|
||||
Bonjour, je suis le Product Manager et j'ai finalisé le PRD pour un portfolio développeur web.
|
||||
|
||||
Le document se trouve dans docs/prd.md.
|
||||
|
||||
Stack technique validé :
|
||||
- PHP natif (pas de framework)
|
||||
- Tailwind CSS avec CLI build
|
||||
- JavaScript vanilla
|
||||
- Fichiers JSON pour les données (projets, témoignages)
|
||||
- Router PHP simple pour URLs propres
|
||||
- Hébergement sur serveur personnel
|
||||
|
||||
Points d'attention :
|
||||
- Router PHP léger (<50 lignes) pour URLs type /projet/{slug}
|
||||
- Structure JSON pour le mini-CMS projets
|
||||
- Formulaire de contact avec reCAPTCHA v3 et envoi email PHP
|
||||
- Optimisation performance (Lighthouse > 90)
|
||||
|
||||
Merci de créer le document d'architecture basé sur ce PRD.
|
||||
```
|
||||
225
docs/stories/1.1.initialisation-projet.md
Normal file
225
docs/stories/1.1.initialisation-projet.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# Story 1.1: Initialisation du Projet et Structure de Fichiers
|
||||
|
||||
## Status
|
||||
|
||||
review
|
||||
|
||||
## Story
|
||||
|
||||
**As a** développeur,
|
||||
**I want** initialiser la structure du projet avec les dossiers et fichiers de base,
|
||||
**so that** je dispose d'une organisation claire et maintenable dès le départ.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. La structure de dossiers est créée : `pages/`, `templates/`, `includes/`, `api/`, `assets/css/`, `assets/js/`, `assets/img/`, `assets/img/projects/`, `assets/fonts/`, `data/`, `logs/`
|
||||
2. Un fichier `index.php` existe à la racine avec un contenu minimal ("Hello World")
|
||||
3. Un fichier `.gitignore` est configuré (node_modules, vendor, .env, logs, output.css)
|
||||
4. Le projet est initialisé avec Git et un premier commit est effectué
|
||||
5. Les fichiers `.env.example` et `composer.json` sont créés
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [x] **Task 1 : Créer la structure de dossiers** (AC: 1)
|
||||
- [x] Créer le dossier `pages/` pour les pages PHP
|
||||
- [x] Créer le dossier `templates/` pour les composants réutilisables
|
||||
- [x] Créer le dossier `includes/` pour router, functions, handlers
|
||||
- [x] Créer le dossier `api/` pour les endpoints (contact)
|
||||
- [x] Créer le dossier `assets/css/` pour les fichiers CSS
|
||||
- [x] Créer le dossier `assets/js/` pour les fichiers JavaScript
|
||||
- [x] Créer le dossier `assets/img/` et `assets/img/projects/`
|
||||
- [x] Créer le dossier `assets/fonts/` pour les polices
|
||||
- [x] Créer le dossier `data/` pour les fichiers JSON
|
||||
- [x] Créer le dossier `logs/` avec un fichier `.gitkeep`
|
||||
|
||||
- [x] **Task 2 : Créer le fichier index.php** (AC: 2)
|
||||
- [x] Créer `index.php` à la racine
|
||||
- [x] Ajouter un contenu minimal HTML5 avec "Hello World"
|
||||
- [x] Inclure les meta tags viewport pour le responsive
|
||||
|
||||
- [x] **Task 3 : Configurer .gitignore** (AC: 3)
|
||||
- [x] Créer le fichier `.gitignore`
|
||||
- [x] Ajouter les exclusions : `.env`, `vendor/`, `node_modules/`, `logs/*.log`, `assets/css/output.css`
|
||||
- [x] Ajouter les exclusions IDE : `.idea/`, `.vscode/`, `.DS_Store`
|
||||
|
||||
- [x] **Task 4 : Créer les fichiers de configuration** (AC: 5)
|
||||
- [x] Créer `.env.example` avec les variables requises
|
||||
- [x] Créer `composer.json` avec la dépendance `vlucas/phpdotenv`
|
||||
|
||||
- [x] **Task 5 : Initialiser Git** (AC: 4)
|
||||
- [x] Vérifier que le repo Git existe (déjà initialisé)
|
||||
- [x] Effectuer un commit initial avec message descriptif
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Structure de Fichiers Cible
|
||||
|
||||
```
|
||||
/portfolio
|
||||
├── index.php # Point d'entrée + router front controller
|
||||
├── config.php # Charge .env et définit les constantes
|
||||
├── composer.json # Dépendances PHP
|
||||
├── .env # Variables sensibles (gitignore)
|
||||
├── .env.example # Template sans valeurs sensibles
|
||||
│
|
||||
├── api/
|
||||
│ └── contact.php # Endpoint formulaire de contact
|
||||
│
|
||||
├── pages/
|
||||
│ ├── home.php # Page d'accueil
|
||||
│ ├── projects.php # Liste projets
|
||||
│ ├── project-single.php # Page projet individuelle
|
||||
│ ├── skills.php # Compétences
|
||||
│ ├── about.php # Me Découvrir
|
||||
│ ├── contact.php # Formulaire de contact
|
||||
│ └── 404.php # Page erreur 404
|
||||
│
|
||||
├── templates/
|
||||
│ ├── header.php # <head>, meta tags, CSS
|
||||
│ ├── footer.php # Scripts JS, copyright
|
||||
│ ├── navbar.php # Navigation sticky + CTA
|
||||
│ └── ... # Autres composants
|
||||
│
|
||||
├── includes/
|
||||
│ ├── router.php # Logique de routage
|
||||
│ ├── functions.php # Helpers PHP
|
||||
│ └── contact-handler.php # Traitement formulaire
|
||||
│
|
||||
├── data/
|
||||
│ ├── projects.json # Données des projets
|
||||
│ └── testimonials.json # Témoignages
|
||||
│
|
||||
├── assets/
|
||||
│ ├── css/
|
||||
│ │ ├── input.css # Source Tailwind
|
||||
│ │ └── output.css # CSS compilé (généré)
|
||||
│ ├── js/
|
||||
│ │ └── main.js # Scripts JS
|
||||
│ ├── img/
|
||||
│ │ └── projects/ # Images des projets
|
||||
│ └── fonts/ # Polices (Inter, JetBrains Mono)
|
||||
│
|
||||
├── logs/ # Logs d'erreurs (gitignore)
|
||||
│ └── .gitkeep
|
||||
│
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
### Contenu .env.example
|
||||
|
||||
```env
|
||||
# Application
|
||||
APP_ENV=development
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost:8000
|
||||
|
||||
# reCAPTCHA v3
|
||||
RECAPTCHA_SITE_KEY=your_site_key_here
|
||||
RECAPTCHA_SECRET_KEY=your_secret_key_here
|
||||
|
||||
# Contact Email
|
||||
CONTACT_EMAIL=contact@example.com
|
||||
|
||||
# Sécurité
|
||||
APP_SECRET=your_random_secret_key_here
|
||||
```
|
||||
|
||||
### Contenu composer.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "portfolio/website",
|
||||
"description": "Portfolio développeur web",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=8.0",
|
||||
"vlucas/phpdotenv": "^5.6"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Conventions de Nommage
|
||||
|
||||
| Élément | Convention | Exemple |
|
||||
|---------|------------|---------|
|
||||
| Fichiers PHP pages | kebab-case | `project-single.php` |
|
||||
| Fichiers PHP templates | kebab-case | `project-card.php` |
|
||||
| Fichiers JS | kebab-case | `contact-form.js` |
|
||||
| Dossiers | kebab-case | `assets/img/projects/` |
|
||||
|
||||
## Testing
|
||||
|
||||
### Validation Manuelle
|
||||
|
||||
- [ ] Tous les dossiers existent et sont accessibles
|
||||
- [ ] Le fichier `index.php` s'affiche dans le navigateur (Hello World)
|
||||
- [ ] Le `.gitignore` fonctionne (les fichiers exclus ne sont pas trackés)
|
||||
- [ ] `composer install` s'exécute sans erreur
|
||||
|
||||
### Commande de Test
|
||||
|
||||
```bash
|
||||
# Vérifier la structure
|
||||
ls -la
|
||||
ls -la pages/ templates/ includes/ api/ assets/ data/ logs/
|
||||
|
||||
# Tester le serveur PHP
|
||||
php -S localhost:8000
|
||||
|
||||
# Installer les dépendances
|
||||
composer install
|
||||
```
|
||||
|
||||
## Change Log
|
||||
|
||||
| Date | Version | Description | Author |
|
||||
|------|---------|-------------|--------|
|
||||
| 2026-02-04 | 0.1 | Implementation story 1.1 | Amelia |
|
||||
| 2026-01-22 | 0.1 | Création initiale de la story | Sarah (PO) |
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
GPT-5 Codex
|
||||
|
||||
### Debug Log References
|
||||
|
||||
- tests/structure.test.ps1: Assert-True boolean cast fix
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
- Structure de dossiers créée
|
||||
- index.php avec Hello World et meta viewport
|
||||
- .gitignore configuré (.env, vendor/, node_modules/, logs/*.log, assets/css/output.css, IDE)
|
||||
- composer.json avec dépendance vlucas/phpdotenv
|
||||
- .env.example avec toutes les variables requises
|
||||
- Tests: `powershell -ExecutionPolicy Bypass -File tests/run.ps1`
|
||||
- Commit initial effectué
|
||||
|
||||
### File List
|
||||
|
||||
| Fichier | Action |
|
||||
|---------|--------|
|
||||
| `index.php` | Créé |
|
||||
| `.gitignore` | Créé |
|
||||
| `.env.example` | Créé |
|
||||
| `composer.json` | Créé |
|
||||
| `logs/.gitkeep` | Créé |
|
||||
| `tests/run.ps1` | Créé |
|
||||
| `tests/structure.test.ps1` | Créé |
|
||||
| `pages/` | Dossier créé |
|
||||
| `templates/` | Dossier créé |
|
||||
| `includes/` | Dossier créé |
|
||||
| `api/` | Dossier créé |
|
||||
| `assets/css/` | Dossier créé |
|
||||
| `assets/js/` | Dossier créé |
|
||||
| `assets/img/` | Dossier créé |
|
||||
| `assets/img/projects/` | Dossier créé |
|
||||
| `assets/fonts/` | Dossier créé |
|
||||
| `data/` | Dossier créé |
|
||||
| `logs/` | Dossier créé |
|
||||
|
||||
## QA Results
|
||||
|
||||
_À compléter par le QA agent_
|
||||
445
docs/stories/1.2.configuration-tailwind.md
Normal file
445
docs/stories/1.2.configuration-tailwind.md
Normal file
@@ -0,0 +1,445 @@
|
||||
# Story 1.2: Configuration de Tailwind CSS avec CLI
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** développeur,
|
||||
**I want** configurer Tailwind CSS avec le CLI et le build optimisé,
|
||||
**so that** je bénéficie d'un CSS performant avec purge automatique.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Node.js et npm sont utilisés uniquement pour le build (pas en production)
|
||||
2. `tailwind.config.js` est créé avec les chemins des fichiers PHP à scanner
|
||||
3. Le fichier `assets/css/input.css` contient les directives Tailwind (@tailwind base, components, utilities)
|
||||
4. La commande `npm run build` génère `assets/css/output.css` minifié
|
||||
5. Une commande `npm run dev` permet le développement en temps réel (watch)
|
||||
6. Le fichier CSS généré fait moins de 50kb après purge
|
||||
7. La palette de couleurs personnalisée est configurée (thème sombre)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Initialiser npm et installer les dépendances** (AC: 1)
|
||||
- [] Créer `package.json` avec `npm init -y`
|
||||
- [] Installer Tailwind CSS : `npm install -D tailwindcss postcss autoprefixer`
|
||||
- [] Vérifier que `node_modules/` est dans `.gitignore`
|
||||
|
||||
- [] **Task 2 : Créer la configuration Tailwind** (AC: 2, 7)
|
||||
- [] Créer `tailwind.config.js` avec les chemins PHP à scanner
|
||||
- [] Configurer la palette de couleurs personnalisée (primary, background, surface, text, etc.)
|
||||
- [] Configurer les polices (Inter, JetBrains Mono)
|
||||
- [] Configurer les tailles de texte personnalisées
|
||||
- [] Configurer les ombres personnalisées
|
||||
|
||||
- [] **Task 3 : Créer le fichier CSS source** (AC: 3)
|
||||
- [] Créer `assets/css/input.css`
|
||||
- [] Ajouter les directives `@tailwind base`, `@tailwind components`, `@tailwind utilities`
|
||||
- [] Ajouter les styles de base dans `@layer base`
|
||||
- [] Ajouter les composants réutilisables dans `@layer components` (btn, card, input, badge, etc.)
|
||||
- [] Ajouter les animations dans `@layer utilities`
|
||||
|
||||
- [] **Task 4 : Configurer PostCSS** (AC: 1)
|
||||
- [] Créer `postcss.config.js` avec tailwindcss et autoprefixer
|
||||
|
||||
- [] **Task 5 : Configurer les scripts npm** (AC: 4, 5)
|
||||
- [] Ajouter le script `build` dans package.json
|
||||
- [] Ajouter le script `dev` (watch) dans package.json
|
||||
- [] Tester les deux scripts
|
||||
|
||||
- [] **Task 6 : Valider la taille du CSS** (AC: 6)
|
||||
- [] Exécuter `npm run build`
|
||||
- [] Vérifier que `output.css` < 50kb (6,4 Ko)
|
||||
- [] Vérifier que le purge fonctionne correctement
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Dépendances npm (devDependencies uniquement)
|
||||
|
||||
```json
|
||||
{
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^3.4.0",
|
||||
"postcss": "^8.4.0",
|
||||
"autoprefixer": "^10.4.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration tailwind.config.js Complète
|
||||
|
||||
```javascript
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./*.php',
|
||||
'./pages/**/*.php',
|
||||
'./templates/**/*.php',
|
||||
'./assets/js/**/*.js'
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: '#FA784F',
|
||||
light: '#FB9570',
|
||||
dark: '#E5623A',
|
||||
},
|
||||
background: '#17171F',
|
||||
surface: {
|
||||
DEFAULT: '#1E1E28',
|
||||
light: '#2A2A36',
|
||||
},
|
||||
border: '#3A3A48',
|
||||
text: {
|
||||
primary: '#F5F5F7',
|
||||
secondary: '#A1A1AA',
|
||||
muted: '#71717A',
|
||||
},
|
||||
success: '#34D399',
|
||||
warning: '#FBBF24',
|
||||
error: '#F87171',
|
||||
info: '#60A5FA',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||
mono: ['JetBrains Mono', 'monospace'],
|
||||
},
|
||||
fontSize: {
|
||||
'display': ['2.5rem', { lineHeight: '1.2', fontWeight: '700' }],
|
||||
'heading': ['2rem', { lineHeight: '1.3', fontWeight: '600' }],
|
||||
'subheading': ['1.5rem', { lineHeight: '1.4', fontWeight: '600' }],
|
||||
'body': ['1rem', { lineHeight: '1.6', fontWeight: '400' }],
|
||||
'small': ['0.875rem', { lineHeight: '1.5', fontWeight: '400' }],
|
||||
},
|
||||
maxWidth: {
|
||||
'content': '1280px',
|
||||
},
|
||||
boxShadow: {
|
||||
'card': '0 4px 20px rgba(0, 0, 0, 0.25)',
|
||||
'card-hover': '0 10px 40px rgba(0, 0, 0, 0.3)',
|
||||
'input-focus': '0 0 0 3px rgba(250, 120, 79, 0.2)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration postcss.config.js
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Contenu assets/css/input.css
|
||||
|
||||
```css
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
@apply scroll-smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-text-primary font-sans antialiased;
|
||||
}
|
||||
|
||||
h1 { @apply text-display text-text-primary; }
|
||||
h2 { @apply text-heading text-text-primary; }
|
||||
h3 { @apply text-subheading text-text-primary; }
|
||||
p { @apply text-body text-text-secondary; }
|
||||
|
||||
a {
|
||||
@apply text-primary hover:text-primary-light transition-colors duration-150;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
@apply outline-none ring-2 ring-primary ring-offset-2 ring-offset-background;
|
||||
}
|
||||
|
||||
::selection {
|
||||
@apply bg-primary/30 text-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Container */
|
||||
.container-content {
|
||||
@apply max-w-content mx-auto px-4 sm:px-6 lg:px-8;
|
||||
}
|
||||
|
||||
/* Boutons */
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center gap-2
|
||||
px-6 py-3 font-medium rounded-lg
|
||||
transition-all duration-150
|
||||
focus:outline-none focus:ring-2 focus:ring-offset-2
|
||||
focus:ring-offset-background
|
||||
disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply btn bg-primary text-background
|
||||
hover:bg-primary-light active:bg-primary-dark
|
||||
focus:ring-primary;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply btn border-2 border-primary text-primary bg-transparent
|
||||
hover:bg-primary hover:text-background
|
||||
focus:ring-primary;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@apply btn text-primary bg-transparent
|
||||
hover:text-primary-light hover:bg-surface-light
|
||||
focus:ring-primary;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
@apply inline-flex items-center px-2.5 py-1
|
||||
text-xs font-medium rounded
|
||||
bg-surface-light text-text-secondary;
|
||||
}
|
||||
|
||||
.badge-primary {
|
||||
@apply bg-primary/20 text-primary;
|
||||
}
|
||||
|
||||
.badge-muted {
|
||||
@apply bg-border text-text-muted;
|
||||
}
|
||||
|
||||
/* Cartes */
|
||||
.card {
|
||||
@apply bg-surface rounded-lg overflow-hidden
|
||||
border border-border/50
|
||||
transition-all duration-200;
|
||||
}
|
||||
|
||||
.card-interactive {
|
||||
@apply card cursor-pointer
|
||||
hover:-translate-y-1 hover:shadow-card-hover
|
||||
hover:border-border;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
@apply p-4 sm:p-6;
|
||||
}
|
||||
|
||||
/* Inputs */
|
||||
.input {
|
||||
@apply w-full px-4 py-3
|
||||
bg-surface border border-border rounded-lg
|
||||
text-text-primary placeholder-text-muted
|
||||
transition-all duration-150
|
||||
focus:outline-none focus:border-primary focus:shadow-input-focus;
|
||||
}
|
||||
|
||||
.input-error {
|
||||
@apply border-error focus:border-error
|
||||
focus:shadow-[0_0_0_3px_rgba(248,113,113,0.2)];
|
||||
}
|
||||
|
||||
.textarea {
|
||||
@apply input min-h-[150px] resize-y;
|
||||
}
|
||||
|
||||
.label {
|
||||
@apply block text-sm font-medium text-text-secondary mb-2;
|
||||
}
|
||||
|
||||
.label-required::after {
|
||||
content: '*';
|
||||
@apply text-error ml-1;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
@apply text-sm text-error mt-1.5 flex items-center gap-1;
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
.section {
|
||||
@apply py-16 sm:py-24;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
@apply text-center mb-12;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@apply text-heading mb-4;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
@apply text-body text-text-secondary max-w-2xl mx-auto;
|
||||
}
|
||||
|
||||
/* Témoignage */
|
||||
.testimonial {
|
||||
@apply bg-surface-light rounded-lg p-6 border-l-4 border-primary;
|
||||
}
|
||||
|
||||
/* Breadcrumb */
|
||||
.breadcrumb {
|
||||
@apply flex items-center gap-2 text-sm text-text-muted;
|
||||
}
|
||||
|
||||
.breadcrumb-link {
|
||||
@apply text-text-secondary hover:text-primary transition-colors;
|
||||
}
|
||||
|
||||
.breadcrumb-current {
|
||||
@apply text-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animation-delay-100 { animation-delay: 100ms; }
|
||||
.animation-delay-200 { animation-delay: 200ms; }
|
||||
.animation-delay-300 { animation-delay: 300ms; }
|
||||
|
||||
.aspect-thumbnail {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Accessibilité - Réduction de mouvement */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scripts package.json
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "tailwindcss -i ./assets/css/input.css -o ./assets/css/output.css --watch",
|
||||
"build": "tailwindcss -i ./assets/css/input.css -o ./assets/css/output.css --minify"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Palette de Couleurs (Thème Sombre)
|
||||
|
||||
| Couleur | Hex | Usage |
|
||||
|---------|-----|-------|
|
||||
| Primary | `#FA784F` | CTA, liens, accents |
|
||||
| Primary Light | `#FB9570` | Hover |
|
||||
| Primary Dark | `#E5623A` | Active/pressed |
|
||||
| Background | `#17171F` | Fond de page |
|
||||
| Surface | `#1E1E28` | Cartes, navbar |
|
||||
| Surface Light | `#2A2A36` | Hover cartes |
|
||||
| Border | `#3A3A48` | Bordures |
|
||||
| Text Primary | `#F5F5F7` | Titres |
|
||||
| Text Secondary | `#A1A1AA` | Texte secondaire |
|
||||
| Text Muted | `#71717A` | Placeholders |
|
||||
|
||||
## Testing
|
||||
|
||||
### Validation Manuelle
|
||||
|
||||
- [ ] `npm install` s'exécute sans erreur
|
||||
- [ ] `npm run build` génère `output.css`
|
||||
- [ ] `npm run dev` lance le watch mode
|
||||
- [ ] Le fichier `output.css` < 50kb
|
||||
- [ ] Les classes Tailwind fonctionnent dans le navigateur
|
||||
|
||||
### Commandes de Test
|
||||
|
||||
```bash
|
||||
# Installer les dépendances
|
||||
npm install
|
||||
|
||||
# Build production
|
||||
npm run build
|
||||
|
||||
# Vérifier la taille du CSS
|
||||
ls -lh assets/css/output.css
|
||||
|
||||
# Lancer le watch pour le dev
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Test Visuel
|
||||
|
||||
Créer un fichier HTML temporaire pour tester les classes :
|
||||
- `.btn-primary` → bouton orange
|
||||
- `.bg-background` → fond sombre #17171F
|
||||
- `.text-primary` → texte orange
|
||||
- `.card` → carte avec fond surface
|
||||
|
||||
## Change Log
|
||||
|
||||
| Date | Version | Description | Author |
|
||||
|------|---------|-------------|--------|
|
||||
| 2026-01-22 | 0.1 | Création initiale de la story | Sarah (PO) |
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### Debug Log References
|
||||
|
||||
_À compléter par le dev agent_
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
- Package.json mis à jour (v2.0.0) avec scripts dev/build Tailwind
|
||||
- Dépendances installées : tailwindcss ^3.4.0, postcss ^8.4.0, autoprefixer ^10.4.0
|
||||
- tailwind.config.js créé avec palette couleurs sombre, polices Inter/JetBrains Mono
|
||||
- input.css complet avec @layer base, components (btn, card, input, badge, etc.), utilities (animations)
|
||||
- postcss.config.js configuré
|
||||
- Build validé : output.css = 6,4 Ko (< 50 Ko requis)
|
||||
- Warning normal : pas encore de fichiers PHP utilisant les classes
|
||||
|
||||
### File List
|
||||
|
||||
| Fichier | Action |
|
||||
|---------|--------|
|
||||
| `package.json` | Modifié |
|
||||
| `package-lock.json` | Modifié |
|
||||
| `tailwind.config.js` | Créé |
|
||||
| `postcss.config.js` | Créé |
|
||||
| `assets/css/input.css` | Créé |
|
||||
| `assets/css/output.css` | Généré |
|
||||
|
||||
## QA Results
|
||||
|
||||
_À compléter par le QA agent_
|
||||
272
docs/stories/1.3.templates-php-base.md
Normal file
272
docs/stories/1.3.templates-php-base.md
Normal file
@@ -0,0 +1,272 @@
|
||||
# Story 1.3: Templates PHP de Base (Header/Footer)
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** développeur,
|
||||
**I want** créer les templates PHP réutilisables pour le header et le footer,
|
||||
**so that** je ne duplique pas le code HTML commun sur chaque page.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. `templates/header.php` contient le doctype, head, meta tags SEO de base, et lien vers le CSS
|
||||
2. `templates/footer.php` contient la fermeture body, les scripts JS, et le copyright
|
||||
3. Le header inclut les balises meta viewport pour le responsive
|
||||
4. Le header permet de passer un titre de page dynamique via une variable PHP
|
||||
5. Les templates sont inclus dans `index.php` et la page s'affiche correctement
|
||||
6. Une fonction helper `include_template()` est créée pour inclure les templates avec des données
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Créer la fonction helper include_template()** (AC: 6)
|
||||
- [] Créer le fichier `includes/functions.php`
|
||||
- [] Implémenter la fonction `include_template($name, $data = [])`
|
||||
- [] La fonction doit utiliser `extract()` pour passer les variables au template
|
||||
- [] Gérer le chemin vers le dossier templates/
|
||||
|
||||
- [] **Task 2 : Créer le template header.php** (AC: 1, 3, 4)
|
||||
- [] Créer `templates/header.php`
|
||||
- [] Ajouter le doctype HTML5
|
||||
- [] Ajouter les meta tags essentiels (charset, viewport, description)
|
||||
- [] Ajouter les meta tags Open Graph de base
|
||||
- [] Ajouter le lien vers `output.css`
|
||||
- [] Ajouter le preload des polices
|
||||
- [] Permettre un titre dynamique via `$pageTitle`
|
||||
- [] Permettre une description dynamique via `$pageDescription`
|
||||
|
||||
- [] **Task 3 : Créer le template footer.php** (AC: 2)
|
||||
- [] Créer `templates/footer.php`
|
||||
- [] Ajouter le copyright avec l'année dynamique
|
||||
- [] Ajouter le lien vers `main.js` avec attribut `defer`
|
||||
- [] Fermer les balises body et html
|
||||
|
||||
- [] **Task 4 : Mettre à jour index.php** (AC: 5)
|
||||
- [] Inclure `includes/functions.php`
|
||||
- [] Utiliser `include_template('header', ['pageTitle' => '...'])`
|
||||
- [] Ajouter un contenu de test minimal
|
||||
- [] Utiliser `include_template('footer')`
|
||||
|
||||
- [] **Task 5 : Tester l'affichage**
|
||||
- [] Lancer le serveur PHP local
|
||||
- [] Vérifier que la page s'affiche correctement
|
||||
- [] Vérifier le titre dans l'onglet du navigateur
|
||||
- [] Vérifier que le CSS est chargé
|
||||
- [] Valider le HTML avec W3C Validator
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Fonction include_template()
|
||||
|
||||
```php
|
||||
// includes/functions.php
|
||||
|
||||
/**
|
||||
* Inclut un template avec des données
|
||||
* @param string $name Nom du template (sans .php)
|
||||
* @param array $data Variables à passer au template
|
||||
*/
|
||||
function include_template(string $name, array $data = []): void
|
||||
{
|
||||
extract($data);
|
||||
include __DIR__ . "/../templates/{$name}.php";
|
||||
}
|
||||
```
|
||||
|
||||
### Contenu templates/header.php
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Template Header
|
||||
* Variables disponibles :
|
||||
* - $pageTitle (string) : Titre de la page
|
||||
* - $pageDescription (string, optionnel) : Meta description
|
||||
*/
|
||||
|
||||
$pageTitle = $pageTitle ?? 'Portfolio - Développeur Web';
|
||||
$pageDescription = $pageDescription ?? 'Portfolio de développeur web full-stack. Découvrez mes projets, compétences et parcours.';
|
||||
$siteName = 'Portfolio';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="<?= htmlspecialchars($pageDescription, ENT_QUOTES, 'UTF-8') ?>">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="<?= htmlspecialchars($pageTitle, ENT_QUOTES, 'UTF-8') ?>">
|
||||
<meta property="og:description" content="<?= htmlspecialchars($pageDescription, ENT_QUOTES, 'UTF-8') ?>">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:locale" content="fr_FR">
|
||||
|
||||
<!-- Preload fonts -->
|
||||
<link rel="preload" href="/assets/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/assets/fonts/jetbrains-mono-var.woff2" as="font" type="font/woff2" crossorigin>
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" href="/assets/img/favicon.ico" type="image/x-icon">
|
||||
|
||||
<!-- CSS -->
|
||||
<link rel="stylesheet" href="/assets/css/output.css">
|
||||
|
||||
<title><?= htmlspecialchars($pageTitle, ENT_QUOTES, 'UTF-8') ?> | <?= $siteName ?></title>
|
||||
</head>
|
||||
<body class="bg-background text-text-primary font-sans antialiased">
|
||||
```
|
||||
|
||||
### Contenu templates/footer.php
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Template Footer
|
||||
*/
|
||||
$currentYear = date('Y');
|
||||
?>
|
||||
<!-- Footer -->
|
||||
<footer class="bg-surface border-t border-border py-8 mt-auto">
|
||||
<div class="container-content text-center">
|
||||
<p class="text-text-muted text-sm">
|
||||
© <?= $currentYear ?> Portfolio. Tous droits réservés.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/assets/js/main.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Structure index.php mise à jour
|
||||
|
||||
```php
|
||||
<?php
|
||||
// index.php - Point d'entrée
|
||||
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
|
||||
// Inclure le header avec le titre
|
||||
include_template('header', [
|
||||
'pageTitle' => 'Accueil',
|
||||
'pageDescription' => 'Portfolio de développeur web. Découvrez mes projets et compétences.'
|
||||
]);
|
||||
?>
|
||||
|
||||
<main class="min-h-screen">
|
||||
<div class="container-content py-20">
|
||||
<h1 class="text-display text-center">Hello World</h1>
|
||||
<p class="text-center text-text-secondary mt-4">
|
||||
Le portfolio est en construction.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php include_template('footer'); ?>
|
||||
```
|
||||
|
||||
### Conventions pour les Variables de Template
|
||||
|
||||
| Variable | Type | Obligatoire | Description |
|
||||
|----------|------|-------------|-------------|
|
||||
| `$pageTitle` | string | Oui | Titre affiché dans l'onglet |
|
||||
| `$pageDescription` | string | Non | Meta description SEO |
|
||||
| `$bodyClass` | string | Non | Classes additionnelles sur body |
|
||||
|
||||
### Meta Tags SEO Inclus
|
||||
|
||||
- `charset` : UTF-8
|
||||
- `viewport` : responsive mobile-first
|
||||
- `description` : description de la page
|
||||
- `og:title` : titre Open Graph
|
||||
- `og:description` : description Open Graph
|
||||
- `og:type` : website
|
||||
- `og:locale` : fr_FR
|
||||
|
||||
### Fichier main.js Minimal
|
||||
|
||||
Créer `assets/js/main.js` avec un contenu minimal pour éviter l'erreur 404 :
|
||||
|
||||
```javascript
|
||||
// assets/js/main.js
|
||||
// Script principal du portfolio
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('Portfolio chargé');
|
||||
});
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Validation Manuelle
|
||||
|
||||
- [ ] La page index.php s'affiche sans erreur PHP
|
||||
- [ ] Le titre de l'onglet affiche "Accueil | Portfolio"
|
||||
- [ ] Le fond est sombre (#17171F)
|
||||
- [ ] Le texte "Hello World" est visible et stylé
|
||||
- [ ] Pas d'erreur 404 dans la console (CSS, JS)
|
||||
- [ ] Le HTML est valide (W3C Validator)
|
||||
|
||||
### Commandes de Test
|
||||
|
||||
```bash
|
||||
# Lancer le serveur PHP
|
||||
php -S localhost:8000
|
||||
|
||||
# Ouvrir dans le navigateur
|
||||
# http://localhost:8000
|
||||
|
||||
# Vérifier les erreurs dans la console DevTools
|
||||
```
|
||||
|
||||
### Checklist Accessibilité
|
||||
|
||||
- [ ] `lang="fr"` sur la balise html
|
||||
- [ ] Meta viewport présent
|
||||
- [ ] Titre de page descriptif
|
||||
- [ ] Structure sémantique (main, footer)
|
||||
|
||||
## Change Log
|
||||
|
||||
| Date | Version | Description | Author |
|
||||
|------|---------|-------------|--------|
|
||||
| 2026-01-22 | 0.1 | Création initiale de la story | Sarah (PO) |
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### Debug Log References
|
||||
|
||||
_À compléter par le dev agent_
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
- Fonction include_template() créée avec extract() pour les variables
|
||||
- header.php avec doctype, meta SEO, Open Graph, preload fonts, CSS link
|
||||
- footer.php avec copyright dynamique et script main.js defer
|
||||
- index.php mis à jour pour utiliser les templates
|
||||
- main.js créé (minimal) pour éviter erreur 404
|
||||
- Syntaxe PHP validée sans erreurs
|
||||
- CSS regénéré avec nouvelles classes (7,7 Ko)
|
||||
|
||||
### File List
|
||||
|
||||
| Fichier | Action |
|
||||
|---------|--------|
|
||||
| `includes/functions.php` | Créé |
|
||||
| `templates/header.php` | Créé |
|
||||
| `templates/footer.php` | Créé |
|
||||
| `assets/js/main.js` | Créé |
|
||||
| `index.php` | Modifié |
|
||||
| `assets/css/output.css` | Regénéré |
|
||||
|
||||
## QA Results
|
||||
|
||||
_À compléter par le QA agent_
|
||||
333
docs/stories/1.4.page-canary-deploiement.md
Normal file
333
docs/stories/1.4.page-canary-deploiement.md
Normal file
@@ -0,0 +1,333 @@
|
||||
# Story 1.4: Page Canary et Validation du Déploiement
|
||||
|
||||
## Status
|
||||
|
||||
In Progress
|
||||
|
||||
## Story
|
||||
|
||||
**As a** développeur,
|
||||
**I want** déployer une page "canary" minimale sur le serveur,
|
||||
**so that** je valide que l'infrastructure PHP et le CSS fonctionnent en production.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. La page index.php affiche un message de test stylé avec Tailwind (titre centré, couleur d'accent)
|
||||
2. La page est responsive (vérifiable sur mobile)
|
||||
3. Le déploiement sur le serveur personnel fonctionne
|
||||
4. Le site est accessible via HTTPS
|
||||
5. Le temps de chargement est inférieur à 2 secondes (Lighthouse)
|
||||
6. Les fichiers sensibles ne sont pas accessibles (.env, vendor/, data/)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Finaliser la page canary** (AC: 1, 2)
|
||||
- [] Mettre à jour `index.php` avec un contenu de test attractif
|
||||
- [] Ajouter un titre centré avec la classe `text-primary`
|
||||
- [] Ajouter un sous-titre et une description
|
||||
- [] Vérifier le responsive sur mobile (DevTools)
|
||||
- [] Tester les classes Tailwind (btn-primary, card, etc.)
|
||||
|
||||
- [] **Task 2 : Préparer les fichiers pour le déploiement** (AC: 3)
|
||||
- [] Exécuter `npm run build` pour générer le CSS minifié
|
||||
- [] Exécuter `composer install --no-dev` pour les dépendances
|
||||
- [ ] Créer le fichier `.env` de production (à faire sur le serveur)
|
||||
- [] Vérifier que `.gitignore` exclut les fichiers sensibles
|
||||
|
||||
- [ ] **Task 3 : Configurer le serveur nginx** (AC: 3, 4, 6)
|
||||
- [ ] Créer/adapter la configuration nginx
|
||||
- [ ] Configurer les redirections vers index.php (front controller)
|
||||
- [ ] Bloquer l'accès aux fichiers sensibles (.env, vendor/, data/, logs/)
|
||||
- [ ] Configurer les headers de sécurité
|
||||
- [ ] Activer la compression gzip
|
||||
|
||||
- [ ] **Task 4 : Configurer HTTPS** (AC: 4)
|
||||
- [ ] Vérifier le certificat SSL (Let's Encrypt)
|
||||
- [ ] Configurer la redirection HTTP → HTTPS
|
||||
- [ ] Tester l'accès HTTPS
|
||||
|
||||
- [ ] **Task 5 : Déployer sur le serveur** (AC: 3)
|
||||
- [ ] Transférer les fichiers (FTP/SFTP ou git pull)
|
||||
- [ ] Exclure : `node_modules/`, `package.json`, `package-lock.json`, `tailwind.config.js`, `postcss.config.js`
|
||||
- [ ] Vérifier les permissions des dossiers
|
||||
- [ ] Tester l'accès à la page
|
||||
|
||||
- [ ] **Task 6 : Valider les performances** (AC: 5)
|
||||
- [ ] Lancer un audit Lighthouse
|
||||
- [ ] Vérifier que le temps de chargement < 2s
|
||||
- [ ] Vérifier le score Performance > 90
|
||||
- [ ] Corriger les éventuels problèmes
|
||||
|
||||
- [ ] **Task 7 : Tests de sécurité** (AC: 6)
|
||||
- [ ] Tester l'accès à `/.env` → doit retourner 404
|
||||
- [ ] Tester l'accès à `/vendor/` → doit retourner 404
|
||||
- [ ] Tester l'accès à `/data/` → doit retourner 404
|
||||
- [ ] Tester l'accès à `/logs/` → doit retourner 404
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Contenu Page Canary (index.php)
|
||||
|
||||
```php
|
||||
<?php
|
||||
// index.php - Page Canary
|
||||
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
|
||||
include_template('header', [
|
||||
'pageTitle' => 'Portfolio en construction',
|
||||
'pageDescription' => 'Mon portfolio de développeur web arrive bientôt. Restez connectés !'
|
||||
]);
|
||||
?>
|
||||
|
||||
<main class="min-h-screen flex items-center justify-center">
|
||||
<div class="container-content text-center py-20">
|
||||
<!-- Titre principal -->
|
||||
<h1 class="text-display text-text-primary mb-4 animate-fade-in">
|
||||
Portfolio <span class="text-primary">en construction</span>
|
||||
</h1>
|
||||
|
||||
<!-- Sous-titre -->
|
||||
<p class="text-xl text-text-secondary mb-8 max-w-2xl mx-auto animate-fade-in animation-delay-100">
|
||||
Je prépare quelque chose de génial pour vous.
|
||||
<br>Revenez bientôt pour découvrir mes projets !
|
||||
</p>
|
||||
|
||||
<!-- Badge de test -->
|
||||
<div class="flex justify-center gap-4 mb-12 animate-fade-in animation-delay-200">
|
||||
<span class="badge">PHP</span>
|
||||
<span class="badge">Tailwind CSS</span>
|
||||
<span class="badge badge-primary">En cours</span>
|
||||
</div>
|
||||
|
||||
<!-- Card de test -->
|
||||
<div class="card max-w-md mx-auto animate-fade-in animation-delay-300">
|
||||
<div class="card-body">
|
||||
<h3 class="text-subheading mb-2">Infrastructure validée</h3>
|
||||
<p class="text-text-secondary mb-4">
|
||||
PHP, Tailwind CSS et le serveur fonctionnent correctement.
|
||||
</p>
|
||||
<div class="flex gap-4 justify-center">
|
||||
<span class="btn-primary">Bouton Primary</span>
|
||||
<span class="btn-secondary">Bouton Secondary</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test responsive -->
|
||||
<p class="text-text-muted text-sm mt-12">
|
||||
Testé sur mobile, tablette et desktop.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php include_template('footer'); ?>
|
||||
```
|
||||
|
||||
### Configuration Nginx (Production)
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name monportfolio.fr www.monportfolio.fr;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name monportfolio.fr www.monportfolio.fr;
|
||||
|
||||
# SSL
|
||||
ssl_certificate /etc/letsencrypt/live/monportfolio.fr/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/monportfolio.fr/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
|
||||
root /var/www/portfolio;
|
||||
index index.php;
|
||||
charset utf-8;
|
||||
|
||||
# Headers de sécurité
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# Bloquer fichiers sensibles
|
||||
location ~ /\.(env|git|htaccess) { deny all; return 404; }
|
||||
location ^~ /vendor/ { deny all; return 404; }
|
||||
location ^~ /node_modules/ { deny all; return 404; }
|
||||
location ^~ /logs/ { deny all; return 404; }
|
||||
location ^~ /data/ { deny all; return 404; }
|
||||
location ^~ /includes/ { deny all; return 404; }
|
||||
|
||||
# Assets statiques avec cache long
|
||||
location /assets/ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
gzip_static on;
|
||||
}
|
||||
|
||||
# Router PHP (front controller)
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
# PHP-FPM
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
# Compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/javascript application/javascript application/json image/svg+xml;
|
||||
}
|
||||
```
|
||||
|
||||
### Fichiers à Déployer
|
||||
|
||||
**Inclure :**
|
||||
- `index.php`
|
||||
- `config.php`
|
||||
- `composer.json`, `composer.lock`
|
||||
- `vendor/` (après composer install --no-dev)
|
||||
- `pages/`
|
||||
- `templates/`
|
||||
- `includes/`
|
||||
- `api/`
|
||||
- `assets/` (avec output.css généré)
|
||||
- `data/`
|
||||
- `logs/` (dossier vide avec .gitkeep)
|
||||
- `.env` (créé sur le serveur, pas commité)
|
||||
|
||||
**Exclure :**
|
||||
- `node_modules/`
|
||||
- `package.json`, `package-lock.json`
|
||||
- `tailwind.config.js`, `postcss.config.js`
|
||||
- `.env` (local)
|
||||
- `.git/`
|
||||
- `docs/`
|
||||
|
||||
### Variables .env Production
|
||||
|
||||
```env
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_URL=https://monportfolio.fr
|
||||
|
||||
RECAPTCHA_SITE_KEY=votre_cle_site
|
||||
RECAPTCHA_SECRET_KEY=votre_cle_secrete
|
||||
|
||||
CONTACT_EMAIL=contact@monportfolio.fr
|
||||
|
||||
APP_SECRET=une_cle_secrete_aleatoire_de_32_caracteres
|
||||
```
|
||||
|
||||
### Checklist Pré-Déploiement
|
||||
|
||||
- [ ] `npm run build` exécuté (CSS minifié)
|
||||
- [ ] `composer install --no-dev` exécuté
|
||||
- [ ] Fichier `.env` de production prêt
|
||||
- [ ] Configuration nginx testée localement
|
||||
- [ ] Certificat SSL valide
|
||||
|
||||
## Testing
|
||||
|
||||
### Tests Fonctionnels
|
||||
|
||||
- [ ] La page s'affiche correctement en production
|
||||
- [ ] Le titre "Portfolio en construction" est visible
|
||||
- [ ] La couleur d'accent (#FA784F) s'affiche
|
||||
- [ ] Les boutons sont stylés correctement
|
||||
- [ ] Pas d'erreur dans la console
|
||||
|
||||
### Tests Responsive
|
||||
|
||||
- [ ] Mobile (375px) : contenu lisible, pas de scroll horizontal
|
||||
- [ ] Tablette (768px) : mise en page correcte
|
||||
- [ ] Desktop (1280px) : centré avec max-width
|
||||
|
||||
### Tests Performance (Lighthouse)
|
||||
|
||||
| Métrique | Objectif | Maximum |
|
||||
|----------|----------|---------|
|
||||
| Performance | > 90 | > 80 |
|
||||
| Accessibility | > 90 | > 85 |
|
||||
| Best Practices | > 90 | > 85 |
|
||||
| SEO | > 90 | > 85 |
|
||||
| FCP | < 1.5s | < 2.5s |
|
||||
| LCP | < 2.5s | < 4s |
|
||||
|
||||
### Tests Sécurité
|
||||
|
||||
```bash
|
||||
# Tester les accès bloqués
|
||||
curl -I https://monportfolio.fr/.env # Doit retourner 404
|
||||
curl -I https://monportfolio.fr/vendor/ # Doit retourner 404
|
||||
curl -I https://monportfolio.fr/data/ # Doit retourner 404
|
||||
curl -I https://monportfolio.fr/includes/ # Doit retourner 404
|
||||
```
|
||||
|
||||
### Commandes de Déploiement
|
||||
|
||||
```bash
|
||||
# Sur le poste local
|
||||
npm run build
|
||||
composer install --no-dev
|
||||
|
||||
# Transfert (exemple rsync)
|
||||
rsync -avz --exclude='node_modules' --exclude='.git' --exclude='docs' \
|
||||
./ user@serveur:/var/www/portfolio/
|
||||
|
||||
# Sur le serveur
|
||||
sudo nginx -t # Tester la config nginx
|
||||
sudo systemctl reload nginx # Recharger nginx
|
||||
```
|
||||
|
||||
## Change Log
|
||||
|
||||
| Date | Version | Description | Author |
|
||||
|------|---------|-------------|--------|
|
||||
| 2026-01-22 | 0.1 | Création initiale de la story | Sarah (PO) |
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### Debug Log References
|
||||
|
||||
_À compléter par le dev agent_
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
- Page canary créée avec titre animé, badges, card de test, boutons
|
||||
- CSS regénéré (12 Ko minifié)
|
||||
- Dépendances PHP installées (vlucas/phpdotenv)
|
||||
- Configuration nginx exemple créée (nginx.conf.example)
|
||||
- Syntaxe PHP validée
|
||||
|
||||
**Tâches restantes (manuelles) :**
|
||||
- Créer .env de production sur le serveur
|
||||
- Copier nginx.conf.example et adapter pour votre serveur
|
||||
- Déployer les fichiers (rsync/FTP)
|
||||
- Configurer SSL/HTTPS
|
||||
- Tests de sécurité et performance
|
||||
|
||||
### File List
|
||||
|
||||
| Fichier | Action |
|
||||
|---------|--------|
|
||||
| `index.php` | Modifié |
|
||||
| `nginx.conf.example` | Créé |
|
||||
| `vendor/` | Installé |
|
||||
| `composer.lock` | Créé |
|
||||
| `assets/css/output.css` | Regénéré |
|
||||
|
||||
## QA Results
|
||||
|
||||
_À compléter par le QA agent_
|
||||
388
docs/stories/2.1.navbar-responsive.md
Normal file
388
docs/stories/2.1.navbar-responsive.md
Normal file
@@ -0,0 +1,388 @@
|
||||
# Story 2.1: Navbar Responsive avec Menu Mobile
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** visiteur,
|
||||
**I want** disposer d'une navigation claire et accessible sur tous les appareils,
|
||||
**so that** je trouve facilement les sections du portfolio.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. `templates/navbar.php` contient le menu de navigation avec les liens : Accueil, Projets, Compétences, Me Découvrir, Contact
|
||||
2. La navbar est fixe en haut de page (sticky) et reste visible au scroll
|
||||
3. Sur mobile, un menu "hamburger" affiche/masque les liens en JavaScript vanilla
|
||||
4. Le lien de la page active est visuellement distingué (couleur/soulignement)
|
||||
5. La navbar est responsive et s'adapte aux 3 breakpoints (mobile, tablette, desktop)
|
||||
6. La navbar a un effet d'ombre au scroll (optionnel mais recommandé)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Créer le template navbar.php** (AC: 1, 2)
|
||||
- [] Créer `templates/navbar.php`
|
||||
- [] Ajouter la structure HTML sémantique (`<header>`, `<nav>`)
|
||||
- [] Ajouter le logo/nom du site (lien vers accueil)
|
||||
- [] Ajouter les liens de navigation desktop
|
||||
- [] Appliquer les classes Tailwind pour sticky + fond
|
||||
|
||||
- [] **Task 2 : Implémenter le menu mobile hamburger** (AC: 3)
|
||||
- [] Ajouter le bouton hamburger (icône 3 barres)
|
||||
- [] Ajouter le menu mobile (overlay ou slide)
|
||||
- [] Masquer le bouton sur desktop, afficher sur mobile
|
||||
- [] Ajouter l'attribut `aria-expanded` pour l'accessibilité
|
||||
|
||||
- [] **Task 3 : Créer le JavaScript pour le menu mobile** (AC: 3)
|
||||
- [] Créer ou mettre à jour `assets/js/main.js`
|
||||
- [] Implémenter le toggle du menu (ouvert/fermé)
|
||||
- [] Fermer le menu au clic sur un lien
|
||||
- [] Fermer le menu avec la touche Escape
|
||||
- [] Gérer l'état `aria-expanded`
|
||||
|
||||
- [] **Task 4 : Implémenter l'état actif des liens** (AC: 4)
|
||||
- [] Passer la page courante via une variable PHP
|
||||
- [] Appliquer une classe visuelle sur le lien actif
|
||||
- [] Utiliser la couleur primary ou un soulignement
|
||||
|
||||
- [] **Task 5 : Rendre la navbar responsive** (AC: 5)
|
||||
- [] Mobile : logo + hamburger uniquement
|
||||
- [] Tablette/Desktop : tous les liens visibles
|
||||
- [] Vérifier les 3 breakpoints
|
||||
|
||||
- [] **Task 6 : Ajouter l'effet au scroll** (AC: 6)
|
||||
- [] Ajouter une ombre quand la page est scrollée
|
||||
- [] Utiliser JavaScript pour détecter le scroll
|
||||
- [] Transition smooth pour l'effet
|
||||
|
||||
- [] **Task 7 : Intégrer la navbar dans les pages**
|
||||
- [] Modifier `header.php` ou les pages pour inclure la navbar
|
||||
- [] Passer la variable `$currentPage` à la navbar
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Structure HTML de la Navbar
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Template Navbar
|
||||
* Variables disponibles :
|
||||
* - $currentPage (string) : identifiant de la page active ('home', 'projects', 'skills', 'about', 'contact')
|
||||
*/
|
||||
|
||||
$currentPage = $currentPage ?? 'home';
|
||||
|
||||
$navLinks = [
|
||||
['id' => 'home', 'label' => 'Accueil', 'url' => '/'],
|
||||
['id' => 'projects', 'label' => 'Projets', 'url' => '/projets'],
|
||||
['id' => 'skills', 'label' => 'Compétences', 'url' => '/competences'],
|
||||
['id' => 'about', 'label' => 'Me Découvrir', 'url' => '/a-propos'],
|
||||
['id' => 'contact', 'label' => 'Contact', 'url' => '/contact', 'isCta' => true],
|
||||
];
|
||||
?>
|
||||
|
||||
<header id="navbar" class="fixed top-0 left-0 right-0 z-50 bg-background/95 backdrop-blur-sm border-b border-border/50 transition-shadow duration-300">
|
||||
<nav class="container-content" aria-label="Navigation principale">
|
||||
<div class="flex items-center justify-between h-16 lg:h-20">
|
||||
|
||||
<!-- Logo -->
|
||||
<a href="/" class="text-xl font-bold text-text-primary hover:text-primary transition-colors">
|
||||
Portfolio
|
||||
</a>
|
||||
|
||||
<!-- Navigation Desktop -->
|
||||
<ul class="hidden lg:flex items-center gap-1">
|
||||
<?php foreach ($navLinks as $link): ?>
|
||||
<?php if (!empty($link['isCta'])): ?>
|
||||
<li class="ml-4">
|
||||
<a href="<?= $link['url'] ?>" class="btn-primary text-sm">
|
||||
<?= $link['label'] ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php else: ?>
|
||||
<li>
|
||||
<a href="<?= $link['url'] ?>"
|
||||
class="nav-link <?= $currentPage === $link['id'] ? 'nav-link-active' : '' ?>">
|
||||
<?= $link['label'] ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
|
||||
<!-- Bouton Hamburger Mobile -->
|
||||
<button
|
||||
id="mobile-menu-toggle"
|
||||
class="lg:hidden p-2 text-text-primary hover:text-primary transition-colors"
|
||||
aria-label="Ouvrir le menu"
|
||||
aria-expanded="false"
|
||||
aria-controls="mobile-menu"
|
||||
>
|
||||
<!-- Icône Hamburger -->
|
||||
<svg class="w-6 h-6 hamburger-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
|
||||
</svg>
|
||||
<!-- Icône Close (hidden par défaut) -->
|
||||
<svg class="w-6 h-6 close-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Menu Mobile -->
|
||||
<div id="mobile-menu" class="lg:hidden hidden" aria-hidden="true">
|
||||
<ul class="py-4 space-y-2 border-t border-border">
|
||||
<?php foreach ($navLinks as $link): ?>
|
||||
<li>
|
||||
<a href="<?= $link['url'] ?>"
|
||||
class="block py-3 px-4 rounded-lg <?= $currentPage === $link['id'] ? 'bg-surface text-primary' : 'text-text-secondary hover:bg-surface hover:text-text-primary' ?> transition-colors">
|
||||
<?= $link['label'] ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Spacer pour compenser la navbar fixed -->
|
||||
<div class="h-16 lg:h-20"></div>
|
||||
```
|
||||
|
||||
### Classes CSS Additionnelles (input.css)
|
||||
|
||||
Ajouter dans `@layer components` :
|
||||
|
||||
```css
|
||||
/* Navigation links */
|
||||
.nav-link {
|
||||
@apply px-4 py-2 text-sm font-medium text-text-secondary
|
||||
hover:text-text-primary transition-colors rounded-lg
|
||||
hover:bg-surface-light;
|
||||
}
|
||||
|
||||
.nav-link-active {
|
||||
@apply text-primary bg-primary/10;
|
||||
}
|
||||
```
|
||||
|
||||
### JavaScript pour le Menu Mobile
|
||||
|
||||
```javascript
|
||||
// assets/js/main.js
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initMobileMenu();
|
||||
initNavbarScroll();
|
||||
});
|
||||
|
||||
/**
|
||||
* Gestion du menu mobile
|
||||
*/
|
||||
function initMobileMenu() {
|
||||
const toggle = document.getElementById('mobile-menu-toggle');
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
|
||||
if (!toggle || !menu) return;
|
||||
|
||||
const hamburgerIcon = toggle.querySelector('.hamburger-icon');
|
||||
const closeIcon = toggle.querySelector('.close-icon');
|
||||
|
||||
function openMenu() {
|
||||
menu.classList.remove('hidden');
|
||||
menu.setAttribute('aria-hidden', 'false');
|
||||
toggle.setAttribute('aria-expanded', 'true');
|
||||
toggle.setAttribute('aria-label', 'Fermer le menu');
|
||||
hamburgerIcon?.classList.add('hidden');
|
||||
closeIcon?.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
menu.classList.add('hidden');
|
||||
menu.setAttribute('aria-hidden', 'true');
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
toggle.setAttribute('aria-label', 'Ouvrir le menu');
|
||||
hamburgerIcon?.classList.remove('hidden');
|
||||
closeIcon?.classList.add('hidden');
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
const isOpen = toggle.getAttribute('aria-expanded') === 'true';
|
||||
if (isOpen) {
|
||||
closeMenu();
|
||||
} else {
|
||||
openMenu();
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle au clic
|
||||
toggle.addEventListener('click', toggleMenu);
|
||||
|
||||
// Fermer au clic sur un lien
|
||||
menu.querySelectorAll('a').forEach(link => {
|
||||
link.addEventListener('click', closeMenu);
|
||||
});
|
||||
|
||||
// Fermer avec Escape
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && toggle.getAttribute('aria-expanded') === 'true') {
|
||||
closeMenu();
|
||||
toggle.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Fermer si on redimensionne vers desktop
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.innerWidth >= 1024) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Effet d'ombre au scroll
|
||||
*/
|
||||
function initNavbarScroll() {
|
||||
const navbar = document.getElementById('navbar');
|
||||
if (!navbar) return;
|
||||
|
||||
let lastScroll = 0;
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
const currentScroll = window.scrollY;
|
||||
|
||||
if (currentScroll > 10) {
|
||||
navbar.classList.add('shadow-lg');
|
||||
} else {
|
||||
navbar.classList.remove('shadow-lg');
|
||||
}
|
||||
|
||||
lastScroll = currentScroll;
|
||||
}, { passive: true });
|
||||
}
|
||||
```
|
||||
|
||||
### Intégration dans les Pages
|
||||
|
||||
Modifier `index.php` et les futures pages :
|
||||
|
||||
```php
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
|
||||
include_template('header', [
|
||||
'pageTitle' => 'Accueil',
|
||||
]);
|
||||
|
||||
include_template('navbar', [
|
||||
'currentPage' => 'home'
|
||||
]);
|
||||
?>
|
||||
|
||||
<!-- Contenu de la page -->
|
||||
|
||||
<?php include_template('footer'); ?>
|
||||
```
|
||||
|
||||
### Structure de Navigation
|
||||
|
||||
| Lien | URL | ID |
|
||||
|------|-----|----|
|
||||
| Accueil | `/` | home |
|
||||
| Projets | `/projets` | projects |
|
||||
| Compétences | `/competences` | skills |
|
||||
| Me Découvrir | `/a-propos` | about |
|
||||
| Contact (CTA) | `/contact` | contact |
|
||||
|
||||
### Breakpoints Responsive
|
||||
|
||||
| Breakpoint | Comportement |
|
||||
|------------|--------------|
|
||||
| Mobile (< 1024px) | Logo + hamburger, menu caché |
|
||||
| Desktop (≥ 1024px) | Tous les liens visibles |
|
||||
|
||||
### Accessibilité
|
||||
|
||||
- `aria-label` sur le bouton hamburger
|
||||
- `aria-expanded` pour indiquer l'état du menu
|
||||
- `aria-controls` pour lier le bouton au menu
|
||||
- `aria-hidden` sur le menu mobile
|
||||
- Navigation au clavier (Tab, Escape)
|
||||
- Focus visible sur tous les éléments interactifs
|
||||
|
||||
## Testing
|
||||
|
||||
### Tests Fonctionnels
|
||||
|
||||
- [ ] Tous les liens sont présents et cliquables
|
||||
- [ ] La navbar reste visible au scroll (sticky)
|
||||
- [ ] Le lien actif est visuellement distinct
|
||||
- [ ] L'ombre apparaît au scroll
|
||||
|
||||
### Tests Mobile
|
||||
|
||||
- [ ] Le bouton hamburger est visible sur mobile
|
||||
- [ ] Le menu s'ouvre au clic
|
||||
- [ ] Le menu se ferme au clic sur un lien
|
||||
- [ ] Le menu se ferme avec Escape
|
||||
- [ ] L'icône change (hamburger ↔ X)
|
||||
|
||||
### Tests Accessibilité
|
||||
|
||||
- [ ] Navigation complète au clavier
|
||||
- [ ] `aria-expanded` se met à jour
|
||||
- [ ] Focus visible sur tous les éléments
|
||||
- [ ] Lecteur d'écran annonce l'état du menu
|
||||
|
||||
### Tests Responsive
|
||||
|
||||
```
|
||||
Mobile (375px) → hamburger visible, liens cachés
|
||||
Tablet (768px) → hamburger visible, liens cachés
|
||||
Desktop (1024px) → liens visibles, hamburger caché
|
||||
Wide (1440px) → liens visibles, centré
|
||||
```
|
||||
|
||||
## Change Log
|
||||
|
||||
| Date | Version | Description | Author |
|
||||
|------|---------|-------------|--------|
|
||||
| 2026-01-22 | 0.1 | Création initiale de la story | Sarah (PO) |
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### Debug Log References
|
||||
|
||||
_À compléter par le dev agent_
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
- templates/navbar.php créé avec structure sémantique complète
|
||||
- Menu hamburger avec icônes SVG (hamburger/close)
|
||||
- JavaScript vanilla pour toggle menu, fermeture Escape, resize
|
||||
- Effet d'ombre au scroll (shadow-lg)
|
||||
- Classes nav-link et nav-link-active ajoutées à input.css
|
||||
- État actif via variable $currentPage
|
||||
- Accessibilité complète (aria-expanded, aria-controls, aria-hidden)
|
||||
- Spacer pour compenser navbar fixed
|
||||
- CSS regénéré (15 Ko)
|
||||
|
||||
### File List
|
||||
|
||||
| Fichier | Action |
|
||||
|---------|--------|
|
||||
| `templates/navbar.php` | Créé |
|
||||
| `assets/css/input.css` | Modifié (classes nav-link) |
|
||||
| `assets/js/main.js` | Modifié (menu mobile + scroll) |
|
||||
| `index.php` | Modifié (intégration navbar) |
|
||||
| `assets/css/output.css` | Regénéré |
|
||||
|
||||
## QA Results
|
||||
|
||||
_À compléter par le QA agent_
|
||||
120
docs/stories/2.2.bouton-cta-navbar.md
Normal file
120
docs/stories/2.2.bouton-cta-navbar.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Story 2.2: Bouton CTA "Me Contacter" dans la Navbar
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** visiteur,
|
||||
**I want** voir un bouton "Me contacter" visible en permanence,
|
||||
**so that** je puisse initier un contact à tout moment sans chercher.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Le bouton "Me contacter" est stylé différemment des autres liens (bouton rempli, couleur d'accent)
|
||||
2. Le bouton est visible sur desktop ET sur mobile (même dans le menu hamburger)
|
||||
3. Le bouton redirige vers la page contact.php
|
||||
4. Le bouton a un état hover/focus visible
|
||||
5. Le bouton respecte l'accessibilité (focusable, contraste suffisant)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Styler le bouton CTA dans la navbar** (AC: 1)
|
||||
- [] Appliquer la classe `btn-primary` au lien Contact
|
||||
- [] Ajouter un espacement distinct (margin-left)
|
||||
- [] Vérifier le contraste des couleurs
|
||||
|
||||
- [] **Task 2 : Assurer la visibilité mobile** (AC: 2)
|
||||
- [] Vérifier que le CTA est présent dans le menu mobile
|
||||
- [] Styler le CTA différemment dans le menu mobile (full-width ou mis en avant)
|
||||
|
||||
- [] **Task 3 : Configurer le lien** (AC: 3)
|
||||
- [] Le href pointe vers `/contact`
|
||||
- [] Tester la navigation
|
||||
|
||||
- [] **Task 4 : Implémenter les états interactifs** (AC: 4, 5)
|
||||
- [] État hover (couleur plus claire)
|
||||
- [] État focus visible (ring)
|
||||
- [] État active (couleur plus foncée)
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Modification navbar.php
|
||||
|
||||
Le bouton CTA est déjà prévu dans la Story 2.1. Vérifier que :
|
||||
|
||||
```php
|
||||
<?php if (!empty($link['isCta'])): ?>
|
||||
<li class="ml-4">
|
||||
<a href="<?= $link['url'] ?>" class="btn-primary text-sm">
|
||||
<?= $link['label'] ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
### Style du CTA dans le menu mobile
|
||||
|
||||
```php
|
||||
<!-- Dans le menu mobile -->
|
||||
<?php if (!empty($link['isCta'])): ?>
|
||||
<li class="pt-2 border-t border-border mt-2">
|
||||
<a href="<?= $link['url'] ?>" class="btn-primary w-full justify-center">
|
||||
<?= $link['label'] ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php else: ?>
|
||||
<!-- ... liens normaux ... -->
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
### Vérification Contraste
|
||||
|
||||
| Élément | Couleurs | Ratio | Conformité |
|
||||
|---------|----------|-------|------------|
|
||||
| Texte bouton | `#17171F` sur `#FA784F` | 5.2:1 | AA |
|
||||
|
||||
## Testing
|
||||
|
||||
- [ ] Le bouton est orange (#FA784F) et se distingue des autres liens
|
||||
- [ ] Le texte est lisible (fond sombre sur orange)
|
||||
- [ ] Le hover change la couleur vers #FB9570
|
||||
- [ ] Le focus affiche un ring visible
|
||||
- [ ] Le bouton est présent et stylé dans le menu mobile
|
||||
- [ ] La navigation vers /contact fonctionne
|
||||
|
||||
## Change Log
|
||||
|
||||
| Date | Version | Description | Author |
|
||||
|------|---------|-------------|--------|
|
||||
| 2026-01-22 | 0.1 | Création initiale | Sarah (PO) |
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### Debug Log References
|
||||
|
||||
_Aucun_
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
- CTA desktop déjà implémenté en story 2.1 (btn-primary text-sm, ml-4)
|
||||
- Menu mobile amélioré : CTA full-width avec séparateur visuel
|
||||
- États interactifs via classe btn-primary (hover, focus, active)
|
||||
- Lien pointe vers /contact
|
||||
- Contraste vérifié (5.2:1 AA)
|
||||
|
||||
### File List
|
||||
|
||||
| Fichier | Action |
|
||||
|---------|--------|
|
||||
| `templates/navbar.php` | Modifié (CTA mobile) |
|
||||
| `assets/css/output.css` | Regénéré |
|
||||
|
||||
## QA Results
|
||||
|
||||
_À compléter par le QA agent_
|
||||
174
docs/stories/2.3.page-accueil-accroche.md
Normal file
174
docs/stories/2.3.page-accueil-accroche.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Story 2.3: Page d'Accueil avec Accroche
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** visiteur,
|
||||
**I want** voir une page d'accueil avec une accroche claire et engageante,
|
||||
**so that** je comprends immédiatement qui est le développeur et ce qu'il propose.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. La page d'accueil affiche une section "hero" avec : nom/prénom, titre (développeur web), et une phrase d'accroche
|
||||
2. Un bouton secondaire CTA invite à découvrir les projets
|
||||
3. Le design est aéré et la typographie met en valeur l'accroche
|
||||
4. La page inclut la navbar et le footer
|
||||
5. Le contenu est centré et responsive
|
||||
6. Les animations sont subtiles (fade-in au chargement)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Créer la page home.php** (AC: 4)
|
||||
- [] Créer `pages/home.php` (implémenté dans index.php, migration avec routeur)
|
||||
- [] Inclure header, navbar et footer
|
||||
- [ ] Configurer le routeur pour servir cette page sur `/` (story 3.2)
|
||||
|
||||
- [] **Task 2 : Créer la section Hero** (AC: 1, 3)
|
||||
- [] Ajouter le nom/prénom du développeur
|
||||
- [] Ajouter le titre "Développeur Web Full-Stack"
|
||||
- [] Ajouter une phrase d'accroche percutante
|
||||
- [] Centrer verticalement et horizontalement
|
||||
- [] Appliquer la typographie (text-display)
|
||||
|
||||
- [] **Task 3 : Ajouter les CTA** (AC: 2)
|
||||
- [] Bouton principal "Découvrir mes projets" (btn-primary)
|
||||
- [] Bouton secondaire "En savoir plus" (btn-secondary) optionnel
|
||||
- [] Liens vers /projets et /a-propos
|
||||
|
||||
- [] **Task 4 : Rendre responsive** (AC: 5)
|
||||
- [] Mobile : texte plus petit, padding réduit
|
||||
- [] Desktop : taille maximale, centré
|
||||
|
||||
- [] **Task 5 : Ajouter les animations** (AC: 6)
|
||||
- [] Fade-in sur le titre (animate-fade-in)
|
||||
- [] Fade-in décalé sur le sous-titre (animation-delay-100)
|
||||
- [] Fade-in décalé sur les boutons (animation-delay-200)
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Structure pages/home.php
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Page d'accueil
|
||||
*/
|
||||
|
||||
$pageTitle = 'Accueil';
|
||||
$pageDescription = 'Portfolio de développeur web full-stack. Découvrez mes projets, compétences et parcours.';
|
||||
$currentPage = 'home';
|
||||
|
||||
include_template('header', compact('pageTitle', 'pageDescription'));
|
||||
include_template('navbar', compact('currentPage'));
|
||||
?>
|
||||
|
||||
<main>
|
||||
<!-- Hero Section -->
|
||||
<section class="min-h-[calc(100vh-5rem)] flex items-center justify-center">
|
||||
<div class="container-content text-center py-20">
|
||||
<!-- Nom -->
|
||||
<p class="text-primary font-medium mb-4 animate-fade-in">
|
||||
Bonjour, je suis
|
||||
</p>
|
||||
|
||||
<!-- Titre principal -->
|
||||
<h1 class="text-display text-text-primary mb-6 animate-fade-in animation-delay-100">
|
||||
Prénom <span class="text-primary">NOM</span>
|
||||
</h1>
|
||||
|
||||
<!-- Sous-titre -->
|
||||
<p class="text-heading text-text-secondary mb-6 animate-fade-in animation-delay-200">
|
||||
Développeur Web Full-Stack
|
||||
</p>
|
||||
|
||||
<!-- Accroche -->
|
||||
<p class="text-xl text-text-secondary max-w-2xl mx-auto mb-10 animate-fade-in animation-delay-300">
|
||||
Je crée des expériences web modernes, performantes et accessibles.
|
||||
<br>Chaque projet est une opportunité de montrer plutôt que de dire.
|
||||
</p>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center animate-fade-in animation-delay-300">
|
||||
<a href="/projets" class="btn-primary">
|
||||
Découvrir mes projets
|
||||
</a>
|
||||
<a href="/a-propos" class="btn-secondary">
|
||||
En savoir plus
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<?php include_template('footer'); ?>
|
||||
```
|
||||
|
||||
### Mise à jour du Router (index.php)
|
||||
|
||||
```php
|
||||
$router
|
||||
->add('/', 'pages/home.php')
|
||||
// ... autres routes
|
||||
```
|
||||
|
||||
### Responsive
|
||||
|
||||
| Breakpoint | Adaptations |
|
||||
|------------|-------------|
|
||||
| Mobile | text-3xl pour H1, padding réduit |
|
||||
| Desktop | text-display (2.5rem), max-w-2xl pour l'accroche |
|
||||
|
||||
### Animations
|
||||
|
||||
Les classes sont déjà définies dans input.css :
|
||||
- `.animate-fade-in` : opacity 0 → 1
|
||||
- `.animate-fade-in-up` : opacity + translateY
|
||||
- `.animation-delay-100/200/300` : délais
|
||||
|
||||
## Testing
|
||||
|
||||
- [ ] Le nom et titre sont visibles et centrés
|
||||
- [ ] L'accroche est lisible et bien espacée
|
||||
- [ ] Les boutons CTA sont cliquables
|
||||
- [ ] Les animations se jouent au chargement
|
||||
- [ ] La page est responsive (mobile/desktop)
|
||||
- [ ] La navbar et le footer sont présents
|
||||
|
||||
## Change Log
|
||||
|
||||
| Date | Version | Description | Author |
|
||||
|------|---------|-------------|--------|
|
||||
| 2026-01-22 | 0.1 | Création initiale | Sarah (PO) |
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### Debug Log References
|
||||
|
||||
_Aucun_
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
- Hero section créée dans index.php (migration vers pages/home.php avec routeur story 3.2)
|
||||
- Typographie responsive : text-4xl → text-5xl → text-display
|
||||
- Animations fade-in avec délais progressifs (100, 200, 300ms)
|
||||
- CTA : btn-primary (projets) + btn-secondary (à propos)
|
||||
- Centrage vertical avec min-h-[calc(100vh-5rem)] et flex
|
||||
- Header, navbar, footer inclus via compact()
|
||||
|
||||
### File List
|
||||
|
||||
| Fichier | Action |
|
||||
|---------|--------|
|
||||
| `index.php` | Modifié (Hero section) |
|
||||
| `assets/css/output.css` | Regénéré |
|
||||
|
||||
## QA Results
|
||||
|
||||
_À compléter par le QA agent_
|
||||
172
docs/stories/2.4.sections-navigation-rapide.md
Normal file
172
docs/stories/2.4.sections-navigation-rapide.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# Story 2.4: Sections de Navigation Rapide sur l'Accueil
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** visiteur,
|
||||
**I want** voir un aperçu des sections principales sur la page d'accueil,
|
||||
**so that** je navigue rapidement vers ce qui m'intéresse.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Sous le hero, des cartes/blocs présentent les sections : Projets, Compétences, Me Découvrir
|
||||
2. Chaque bloc a un titre, une courte description, et un lien vers la page correspondante
|
||||
3. Les blocs sont disposés en grille responsive (1 colonne mobile, 3 colonnes desktop)
|
||||
4. Les blocs ont un effet hover subtil
|
||||
5. L'ensemble reste cohérent avec le design global
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Ajouter la section sous le hero** (AC: 1)
|
||||
- [] Créer une section avec titre "Explorez mon portfolio"
|
||||
- [] Ajouter les 3 cartes de navigation
|
||||
|
||||
- [] **Task 2 : Créer les cartes de navigation** (AC: 2)
|
||||
- [] Carte Projets : icône, titre, description, lien
|
||||
- [] Carte Compétences : icône, titre, description, lien
|
||||
- [] Carte Me Découvrir : icône, titre, description, lien
|
||||
|
||||
- [] **Task 3 : Implémenter la grille responsive** (AC: 3)
|
||||
- [] 1 colonne sur mobile (grid-cols-1)
|
||||
- [] 3 colonnes sur desktop (md:grid-cols-3)
|
||||
- [] Gap approprié entre les cartes (gap-6 lg:gap-8)
|
||||
|
||||
- [] **Task 4 : Ajouter les effets hover** (AC: 4)
|
||||
- [] Utiliser la classe card-interactive
|
||||
- [] Élévation + ombre au hover
|
||||
|
||||
- [] **Task 5 : Intégrer les icônes** (AC: 5)
|
||||
- [] Utiliser Heroicons (SVG inline)
|
||||
- [] Taille cohérente (w-8 h-8 dans conteneur w-16 h-16)
|
||||
- [] Couleur primary
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Code à ajouter dans pages/home.php
|
||||
|
||||
```php
|
||||
<!-- Après la section Hero -->
|
||||
|
||||
<!-- Section Navigation Rapide -->
|
||||
<section class="section bg-surface">
|
||||
<div class="container-content">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Explorez mon portfolio</h2>
|
||||
<p class="section-subtitle">
|
||||
Découvrez mes réalisations, compétences et parcours
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8">
|
||||
<!-- Carte Projets -->
|
||||
<a href="/projets" class="card-interactive group">
|
||||
<div class="card-body text-center">
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
|
||||
<svg class="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-subheading mb-2 group-hover:text-primary transition-colors">Projets</h3>
|
||||
<p class="text-text-secondary">
|
||||
Découvrez mes réalisations web avec démonstrations et explications techniques.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Carte Compétences -->
|
||||
<a href="/competences" class="card-interactive group">
|
||||
<div class="card-body text-center">
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
|
||||
<svg class="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-subheading mb-2 group-hover:text-primary transition-colors">Compétences</h3>
|
||||
<p class="text-text-secondary">
|
||||
Technologies maîtrisées et outils utilisés, avec preuves à l'appui.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Carte Me Découvrir -->
|
||||
<a href="/a-propos" class="card-interactive group">
|
||||
<div class="card-body text-center">
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
|
||||
<svg class="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-subheading mb-2 group-hover:text-primary transition-colors">Me Découvrir</h3>
|
||||
<p class="text-text-secondary">
|
||||
Mon parcours, mes motivations et ce qui me passionne au-delà du code.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
### Icônes Heroicons Utilisées
|
||||
|
||||
| Carte | Icône | Description |
|
||||
|-------|-------|-------------|
|
||||
| Projets | squares-2x2 | Grille de carrés |
|
||||
| Compétences | code-bracket | Chevrons de code |
|
||||
| Me Découvrir | user | Silhouette utilisateur |
|
||||
|
||||
### Responsive
|
||||
|
||||
| Breakpoint | Colonnes |
|
||||
|------------|----------|
|
||||
| Mobile (< 768px) | 1 colonne |
|
||||
| Tablet (≥ 768px) | 3 colonnes |
|
||||
| Desktop | 3 colonnes avec gap plus large |
|
||||
|
||||
## Testing
|
||||
|
||||
- [ ] Les 3 cartes sont visibles sous le hero
|
||||
- [ ] Chaque carte a une icône, titre et description
|
||||
- [ ] Les liens redirigent vers les bonnes pages
|
||||
- [ ] L'effet hover fonctionne (élévation + ombre)
|
||||
- [ ] Mobile : cartes empilées verticalement
|
||||
- [ ] Desktop : 3 cartes côte à côte
|
||||
|
||||
## Change Log
|
||||
|
||||
| Date | Version | Description | Author |
|
||||
|------|---------|-------------|--------|
|
||||
| 2026-01-22 | 0.1 | Création initiale | Sarah (PO) |
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### Debug Log References
|
||||
|
||||
_Aucun_
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
- Section navigation rapide ajoutée sous le hero dans index.php
|
||||
- 3 cartes : Projets, Compétences, Me Découvrir
|
||||
- Grille responsive : grid-cols-1 mobile, md:grid-cols-3 tablet+
|
||||
- Icônes Heroicons SVG inline (squares-2x2, code-bracket, user)
|
||||
- Effets hover via card-interactive + group-hover sur titres
|
||||
- Conteneurs d'icônes avec bg-primary/10 → bg-primary/20 au hover
|
||||
|
||||
### File List
|
||||
|
||||
| Fichier | Action |
|
||||
|---------|--------|
|
||||
| `index.php` | Modifié (section navigation) |
|
||||
| `assets/css/output.css` | Regénéré |
|
||||
|
||||
## QA Results
|
||||
|
||||
_À compléter par le QA agent_
|
||||
235
docs/stories/3.1.structure-donnees-json.md
Normal file
235
docs/stories/3.1.structure-donnees-json.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# Story 3.1: Structure de Données JSON pour les Projets
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** développeur,
|
||||
**I want** définir et créer le fichier JSON contenant les données des projets,
|
||||
**so that** je centralise les informations et facilite la maintenance.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Le fichier `data/projects.json` est créé avec la structure définie
|
||||
2. La structure supporte : id, title, slug, category (vedette/secondaire), thumbnail, url, technologies[], context, solution, teamwork, duration, testimonial, screenshots[]
|
||||
3. Au moins 2 projets de test sont ajoutés pour valider la structure
|
||||
4. Une fonction PHP `getProjects()` lit et décode le JSON
|
||||
5. La fonction gère les erreurs (fichier manquant, JSON invalide)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Définir la structure JSON** (AC: 2)
|
||||
- [] Documenter tous les champs requis et optionnels
|
||||
- [] Définir les types de données pour chaque champ
|
||||
- [] Définir les valeurs possibles pour category
|
||||
|
||||
- [] **Task 2 : Créer le fichier projects.json** (AC: 1, 3)
|
||||
- [] Créer `data/projects.json`
|
||||
- [] Ajouter 2-3 projets de test
|
||||
- [] Valider la syntaxe JSON
|
||||
|
||||
- [] **Task 3 : Créer les fonctions PHP d'accès** (AC: 4, 5)
|
||||
- [] Créer `loadJsonData()` générique
|
||||
- [] Créer `getProjects()`
|
||||
- [] Créer `getProjectsByCategory()`
|
||||
- [] Créer `getProjectBySlug()`
|
||||
- [] Gérer les erreurs (fichier manquant, JSON invalide)
|
||||
|
||||
- [] **Task 4 : Tester les fonctions**
|
||||
- [] Tester avec fichier valide
|
||||
- [] Tester avec fichier manquant
|
||||
- [] Tester avec JSON invalide
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Structure data/projects.json
|
||||
|
||||
```json
|
||||
{
|
||||
"projects": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Site E-commerce XYZ",
|
||||
"slug": "ecommerce-xyz",
|
||||
"category": "vedette",
|
||||
"thumbnail": "ecommerce-xyz-thumb.webp",
|
||||
"url": "https://example.com",
|
||||
"github": "https://github.com/user/project",
|
||||
"technologies": ["PHP", "JavaScript", "Tailwind CSS", "MySQL"],
|
||||
"context": "Client souhaitant moderniser sa boutique en ligne pour améliorer l'expérience utilisateur et augmenter les conversions.",
|
||||
"solution": "Développement d'une solution e-commerce sur mesure avec panier persistant, paiement sécurisé Stripe, et interface d'administration.",
|
||||
"teamwork": "Projet réalisé en collaboration avec un designer UI/UX. J'ai pris en charge l'intégration et le développement backend.",
|
||||
"duration": "3 mois",
|
||||
"screenshots": [
|
||||
"ecommerce-xyz-screen-1.webp",
|
||||
"ecommerce-xyz-screen-2.webp",
|
||||
"ecommerce-xyz-screen-3.webp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Application de Gestion",
|
||||
"slug": "app-gestion",
|
||||
"category": "vedette",
|
||||
"thumbnail": "app-gestion-thumb.webp",
|
||||
"url": null,
|
||||
"github": "https://github.com/user/app-gestion",
|
||||
"technologies": ["React", "Node.js", "PostgreSQL", "Docker"],
|
||||
"context": "Startup ayant besoin d'un outil interne pour gérer ses ressources et planifier ses projets.",
|
||||
"solution": "Application web full-stack avec authentification, gestion des rôles, tableaux de bord et exports PDF.",
|
||||
"teamwork": null,
|
||||
"duration": "4 mois",
|
||||
"screenshots": [
|
||||
"app-gestion-screen-1.webp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Site Vitrine Restaurant",
|
||||
"slug": "restaurant-vitrine",
|
||||
"category": "secondaire",
|
||||
"thumbnail": "restaurant-thumb.webp",
|
||||
"url": "https://restaurant-example.com",
|
||||
"github": null,
|
||||
"technologies": ["HTML", "CSS", "JavaScript"],
|
||||
"context": "Restaurant local souhaitant une présence en ligne simple.",
|
||||
"solution": null,
|
||||
"teamwork": null,
|
||||
"duration": "2 semaines",
|
||||
"screenshots": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Champs du Projet
|
||||
|
||||
| Champ | Type | Requis | Description |
|
||||
|-------|------|--------|-------------|
|
||||
| id | number | Oui | Identifiant unique |
|
||||
| title | string | Oui | Titre du projet |
|
||||
| slug | string | Oui | URL-friendly (unique) |
|
||||
| category | string | Oui | "vedette" ou "secondaire" |
|
||||
| thumbnail | string | Oui | Nom du fichier image |
|
||||
| url | string/null | Non | URL du projet en ligne |
|
||||
| github | string/null | Non | URL du repo GitHub |
|
||||
| technologies | array | Oui | Liste des technos |
|
||||
| context | string | Oui | Description du contexte |
|
||||
| solution | string/null | Non | Description technique |
|
||||
| teamwork | string/null | Non | Travail d'équipe |
|
||||
| duration | string | Oui | Durée du projet |
|
||||
| screenshots | array | Non | Liste des captures |
|
||||
|
||||
### Fonctions PHP (includes/functions.php)
|
||||
|
||||
```php
|
||||
/**
|
||||
* Charge et parse un fichier JSON
|
||||
*/
|
||||
function loadJsonData(string $filename): array
|
||||
{
|
||||
$path = __DIR__ . "/../data/{$filename}";
|
||||
|
||||
if (!file_exists($path)) {
|
||||
error_log("JSON file not found: {$filename}");
|
||||
return [];
|
||||
}
|
||||
|
||||
$content = file_get_contents($path);
|
||||
$data = json_decode($content, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
error_log("JSON parse error in {$filename}: " . json_last_error_msg());
|
||||
return [];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère tous les projets
|
||||
*/
|
||||
function getProjects(): array
|
||||
{
|
||||
$data = loadJsonData('projects.json');
|
||||
return $data['projects'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les projets par catégorie
|
||||
*/
|
||||
function getProjectsByCategory(string $category): array
|
||||
{
|
||||
return array_filter(getProjects(), fn($p) => $p['category'] === $category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un projet par son slug
|
||||
*/
|
||||
function getProjectBySlug(string $slug): ?array
|
||||
{
|
||||
$projects = getProjects();
|
||||
foreach ($projects as $project) {
|
||||
if ($project['slug'] === $slug) {
|
||||
return $project;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les technologies uniques de tous les projets
|
||||
*/
|
||||
function getAllTechnologies(): array
|
||||
{
|
||||
$technologies = [];
|
||||
foreach (getProjects() as $project) {
|
||||
foreach ($project['technologies'] ?? [] as $tech) {
|
||||
if (!in_array($tech, $technologies)) {
|
||||
$technologies[] = $tech;
|
||||
}
|
||||
}
|
||||
}
|
||||
sort($technologies);
|
||||
return $technologies;
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
- [] Le fichier JSON est valide (pas d'erreur de syntaxe)
|
||||
- [] `getProjects()` retourne un tableau de projets
|
||||
- [] `getProjectsByCategory('vedette')` retourne les projets vedettes
|
||||
- [] `getProjectBySlug('ecommerce-xyz')` retourne le bon projet
|
||||
- [] `getProjectBySlug('inexistant')` retourne null
|
||||
- [] Fichier manquant → tableau vide, pas d'exception
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### File List
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `data/projects.json` | Created | Fichier JSON avec 3 projets de test |
|
||||
| `includes/functions.php` | Modified | Ajout des fonctions d'accès aux données JSON |
|
||||
|
||||
### Completion Notes
|
||||
- Structure JSON complète avec tous les champs requis et optionnels
|
||||
- 3 projets de test ajoutés (2 vedettes, 1 secondaire)
|
||||
- Fonctions PHP: `loadJsonData()`, `getProjects()`, `getProjectsByCategory()`, `getProjectBySlug()`, `getAllTechnologies()`
|
||||
- Gestion des erreurs: fichier manquant et JSON invalide retournent tableau vide avec log
|
||||
- Tous les tests passent (8/8)
|
||||
|
||||
### 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) |
|
||||
250
docs/stories/3.2.router-php-urls.md
Normal file
250
docs/stories/3.2.router-php-urls.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# Story 3.2: Router PHP et URLs Propres
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** visiteur,
|
||||
**I want** des URLs lisibles et propres pour accéder aux projets,
|
||||
**so that** je comprends le contenu de la page avant même de cliquer et améliore le SEO.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Un fichier `.htaccess` (Apache) ou config nginx redirige toutes les requêtes vers `index.php` (front controller)
|
||||
2. Un router PHP simple parse l'URL et route vers le bon fichier/action
|
||||
3. Les URLs des projets sont au format `/projet/{slug}` (ex: `/projet/site-ecommerce-xyz`)
|
||||
4. Les autres pages gardent des URLs simples : `/projets`, `/competences`, `/a-propos`, `/contact`
|
||||
5. Une route 404 personnalisée gère les URLs inconnues
|
||||
6. Le router est léger (<50 lignes de code) et sans dépendance externe
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Créer le router PHP** (AC: 2, 6)
|
||||
- [] Créer `includes/router.php`
|
||||
- [] Implémenter la classe Router
|
||||
- [] Méthode add() pour ajouter des routes
|
||||
- [] Méthode resolve() pour matcher une URL
|
||||
- [] Méthode dispatch() pour exécuter la route
|
||||
|
||||
- [] **Task 2 : Configurer les routes** (AC: 3, 4)
|
||||
- [] Route `/` → pages/home.php
|
||||
- [] Route `/projets` → pages/projects.php
|
||||
- [] Route `/projet/{slug}` → pages/project-single.php
|
||||
- [] Route `/competences` → pages/skills.php
|
||||
- [] Route `/a-propos` → pages/about.php
|
||||
- [] Route `/contact` → pages/contact.php
|
||||
|
||||
- [] **Task 3 : Créer la page 404** (AC: 5)
|
||||
- [] Créer `pages/404.php`
|
||||
- [] Design cohérent avec le site
|
||||
- [] Lien retour vers l'accueil
|
||||
|
||||
- [] **Task 4 : Configurer le serveur** (AC: 1)
|
||||
- [] Créer `.htaccess` pour Apache
|
||||
- [] Documenter la config nginx équivalente
|
||||
|
||||
- [] **Task 5 : Mettre à jour index.php**
|
||||
- [] Inclure le router
|
||||
- [] Définir toutes les routes
|
||||
- [] Appeler dispatch()
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Router PHP (includes/router.php)
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Router simple pour URLs propres
|
||||
* < 50 lignes de code
|
||||
*/
|
||||
|
||||
class Router
|
||||
{
|
||||
private array $routes = [];
|
||||
|
||||
public function add(string $pattern, string $handler): self
|
||||
{
|
||||
// Convertit {param} en regex (?P<param>[^/]+)
|
||||
$regex = preg_replace('/\{(\w+)\}/', '([^/]+)', $pattern);
|
||||
$regex = '#^' . $regex . '$#';
|
||||
$this->routes[$regex] = $handler;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function resolve(string $uri): array
|
||||
{
|
||||
$uri = parse_url($uri, PHP_URL_PATH);
|
||||
$uri = rtrim($uri, '/') ?: '/';
|
||||
|
||||
foreach ($this->routes as $regex => $handler) {
|
||||
if (preg_match($regex, $uri, $matches)) {
|
||||
array_shift($matches); // Enlève le match complet
|
||||
return [$handler, $matches];
|
||||
}
|
||||
}
|
||||
|
||||
return ['pages/404.php', []];
|
||||
}
|
||||
|
||||
public function dispatch(): void
|
||||
{
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
[$handler, $params] = $this->resolve($uri);
|
||||
|
||||
// Rend les paramètres accessibles
|
||||
$GLOBALS['routeParams'] = $params;
|
||||
|
||||
require __DIR__ . '/../' . $handler;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Point d'entrée (index.php)
|
||||
|
||||
```php
|
||||
<?php
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
require_once __DIR__ . '/includes/router.php';
|
||||
|
||||
session_start();
|
||||
|
||||
$router = new Router();
|
||||
|
||||
$router
|
||||
->add('/', 'pages/home.php')
|
||||
->add('/projets', 'pages/projects.php')
|
||||
->add('/projet/{slug}', 'pages/project-single.php')
|
||||
->add('/competences', 'pages/skills.php')
|
||||
->add('/a-propos', 'pages/about.php')
|
||||
->add('/contact', 'pages/contact.php');
|
||||
|
||||
$router->dispatch();
|
||||
```
|
||||
|
||||
### Configuration .htaccess (Apache)
|
||||
|
||||
```apache
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
|
||||
# Ne pas réécrire les fichiers et dossiers existants
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
|
||||
# Rediriger tout vers index.php
|
||||
RewriteRule ^(.*)$ index.php [QSA,L]
|
||||
```
|
||||
|
||||
### Configuration Nginx
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
```
|
||||
|
||||
### Page 404 (pages/404.php)
|
||||
|
||||
```php
|
||||
<?php
|
||||
http_response_code(404);
|
||||
|
||||
$pageTitle = 'Page non trouvée';
|
||||
$currentPage = '';
|
||||
|
||||
include_template('header', compact('pageTitle'));
|
||||
include_template('navbar', compact('currentPage'));
|
||||
?>
|
||||
|
||||
<main class="min-h-screen flex items-center justify-center">
|
||||
<div class="container-content text-center py-20">
|
||||
<h1 class="text-display text-primary mb-4">404</h1>
|
||||
<p class="text-xl text-text-secondary mb-8">
|
||||
Oups ! Cette page n'existe pas.
|
||||
</p>
|
||||
<a href="/" class="btn-primary">
|
||||
Retour à l'accueil
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php include_template('footer'); ?>
|
||||
```
|
||||
|
||||
### Récupérer les paramètres de route
|
||||
|
||||
```php
|
||||
// Dans pages/project-single.php
|
||||
$slug = $GLOBALS['routeParams'][0] ?? null;
|
||||
$project = getProjectBySlug($slug);
|
||||
|
||||
if (!$project) {
|
||||
http_response_code(404);
|
||||
include __DIR__ . '/404.php';
|
||||
exit;
|
||||
}
|
||||
```
|
||||
|
||||
### Structure des URLs
|
||||
|
||||
| URL | Page | Paramètres |
|
||||
|-----|------|------------|
|
||||
| `/` | home.php | - |
|
||||
| `/projets` | projects.php | - |
|
||||
| `/projet/ecommerce-xyz` | project-single.php | slug=ecommerce-xyz |
|
||||
| `/competences` | skills.php | - |
|
||||
| `/a-propos` | about.php | - |
|
||||
| `/contact` | contact.php | - |
|
||||
| `/nimporte-quoi` | 404.php | - |
|
||||
|
||||
## Testing
|
||||
|
||||
- [] `/` affiche la page d'accueil
|
||||
- [] `/projets` affiche la liste des projets
|
||||
- [] `/projet/ecommerce-xyz` affiche le projet correspondant
|
||||
- [] `/projet/inexistant` affiche la page 404
|
||||
- [] `/page-inexistante` affiche la page 404
|
||||
- [] Les assets (/assets/css/...) sont toujours accessibles
|
||||
- [] Pas de boucle de redirection
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### File List
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `includes/router.php` | Created | Router PHP simple (43 lignes) |
|
||||
| `index.php` | Modified | Converti en front controller |
|
||||
| `.htaccess` | Created | Réécriture URLs Apache |
|
||||
| `pages/home.php` | Created | Page d'accueil |
|
||||
| `pages/projects.php` | Created | Page liste projets (placeholder) |
|
||||
| `pages/project-single.php` | Created | Page projet individuel |
|
||||
| `pages/skills.php` | Created | Page compétences (placeholder) |
|
||||
| `pages/about.php` | Created | Page à propos (placeholder) |
|
||||
| `pages/contact.php` | Created | Page contact (placeholder) |
|
||||
| `pages/404.php` | Created | Page erreur 404 |
|
||||
|
||||
### Completion Notes
|
||||
- Router PHP léger (43 lignes < 50 requis)
|
||||
- Support des paramètres dynamiques {slug}
|
||||
- Trailing slash normalisé automatiquement
|
||||
- 404 pour routes inconnues
|
||||
- Pages placeholder créées pour futures stories
|
||||
- Tous les tests du router passent (8/8)
|
||||
|
||||
### 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) |
|
||||
212
docs/stories/3.3.page-liste-projets.md
Normal file
212
docs/stories/3.3.page-liste-projets.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Story 3.3: Page Liste des Projets Vedettes
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** visiteur,
|
||||
**I want** voir une liste visuelle de tous les projets vedettes,
|
||||
**so that** je parcours rapidement le portfolio et choisis ce qui m'intéresse.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. `/projets` affiche tous les projets où category = "vedette"
|
||||
2. Chaque projet est affiché sous forme de carte avec : thumbnail, titre, technologies (badges)
|
||||
3. Les cartes sont cliquables et redirigent vers `/projet/{slug}`
|
||||
4. La grille est responsive (1 col mobile, 2 cols tablette, 3 cols desktop)
|
||||
5. Les cartes ont un effet hover (légère élévation/ombre)
|
||||
6. Le template `templates/project-card.php` est réutilisable
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Créer la page projects.php** (AC: 1)
|
||||
- [] Créer `pages/projects.php`
|
||||
- [] Récupérer les projets vedettes avec `getProjectsByCategory('vedette')`
|
||||
- [] Inclure header, navbar, footer
|
||||
|
||||
- [] **Task 2 : Créer le template project-card.php** (AC: 2, 6)
|
||||
- [] Créer `templates/project-card.php`
|
||||
- [] Afficher le thumbnail avec lazy loading
|
||||
- [] Afficher le titre
|
||||
- [] Afficher les badges technologies (max 4)
|
||||
- [] Rendre le composant réutilisable
|
||||
|
||||
- [] **Task 3 : Implémenter la grille responsive** (AC: 4)
|
||||
- [] 1 colonne sur mobile
|
||||
- [] 2 colonnes sur tablette (sm:)
|
||||
- [] 3 colonnes sur desktop (lg:)
|
||||
|
||||
- [] **Task 4 : Ajouter les interactions** (AC: 3, 5)
|
||||
- [] Carte entière cliquable (lien vers /projet/{slug})
|
||||
- [] Effet hover avec card-interactive
|
||||
- [] Transition smooth
|
||||
|
||||
- [] **Task 5 : Gérer les cas limites**
|
||||
- [] Aucun projet → message "Projets à venir"
|
||||
- [] Image manquante → placeholder (onerror fallback)
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Page pages/projects.php
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Page liste des projets
|
||||
*/
|
||||
|
||||
$pageTitle = 'Mes Projets';
|
||||
$pageDescription = 'Découvrez mes réalisations web : sites vitrines, e-commerce, applications et plus encore.';
|
||||
$currentPage = 'projects';
|
||||
|
||||
$featuredProjects = getProjectsByCategory('vedette');
|
||||
$secondaryProjects = getProjectsByCategory('secondaire');
|
||||
|
||||
include_template('header', compact('pageTitle', 'pageDescription'));
|
||||
include_template('navbar', compact('currentPage'));
|
||||
?>
|
||||
|
||||
<main>
|
||||
<!-- Header de page -->
|
||||
<section class="section">
|
||||
<div class="container-content">
|
||||
<div class="section-header">
|
||||
<h1 class="section-title">Mes Projets</h1>
|
||||
<p class="section-subtitle">
|
||||
Découvrez les réalisations qui illustrent mon travail et mes compétences.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Grille des projets vedettes -->
|
||||
<?php if (!empty($featuredProjects)): ?>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
|
||||
<?php foreach ($featuredProjects as $project): ?>
|
||||
<?php include_template('project-card', ['project' => $project]); ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p class="text-center text-text-muted py-12">
|
||||
Projets à venir...
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section projets secondaires (Story 3.5) -->
|
||||
</main>
|
||||
|
||||
<?php include_template('footer'); ?>
|
||||
```
|
||||
|
||||
### Template templates/project-card.php
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Carte projet réutilisable
|
||||
* @param array $project Données du projet
|
||||
*/
|
||||
|
||||
$title = $project['title'] ?? 'Sans titre';
|
||||
$slug = $project['slug'] ?? '#';
|
||||
$thumbnail = $project['thumbnail'] ?? 'default-project.webp';
|
||||
$technologies = $project['technologies'] ?? [];
|
||||
$maxTechs = 4;
|
||||
?>
|
||||
|
||||
<article class="card-interactive group">
|
||||
<a href="/projet/<?= htmlspecialchars($slug, ENT_QUOTES, 'UTF-8') ?>" class="block">
|
||||
<!-- Thumbnail -->
|
||||
<div class="aspect-thumbnail overflow-hidden">
|
||||
<picture>
|
||||
<source
|
||||
srcset="/assets/img/projects/<?= htmlspecialchars($thumbnail, ENT_QUOTES, 'UTF-8') ?>"
|
||||
type="image/webp"
|
||||
>
|
||||
<img
|
||||
src="/assets/img/projects/<?= htmlspecialchars(str_replace('.webp', '.jpg', $thumbnail), ENT_QUOTES, 'UTF-8') ?>"
|
||||
alt="Aperçu du projet <?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?>"
|
||||
width="400"
|
||||
height="225"
|
||||
loading="lazy"
|
||||
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
>
|
||||
</picture>
|
||||
</div>
|
||||
|
||||
<!-- Contenu -->
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold text-text-primary mb-3 group-hover:text-primary transition-colors">
|
||||
<?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?>
|
||||
</h3>
|
||||
|
||||
<!-- Technologies (badges) -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<?php foreach (array_slice($technologies, 0, $maxTechs) as $tech): ?>
|
||||
<span class="badge"><?= htmlspecialchars($tech, ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if (count($technologies) > $maxTechs): ?>
|
||||
<span class="badge badge-muted">+<?= count($technologies) - $maxTechs ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
```
|
||||
|
||||
### Grille Responsive
|
||||
|
||||
| Breakpoint | Colonnes | Gap |
|
||||
|------------|----------|-----|
|
||||
| Mobile (< 640px) | 1 | 1.5rem |
|
||||
| Tablet (≥ 640px) | 2 | 1.5rem |
|
||||
| Desktop (≥ 1024px) | 3 | 2rem |
|
||||
|
||||
### Classes Tailwind
|
||||
|
||||
```html
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
- [] La page `/projets` s'affiche correctement
|
||||
- [] Seuls les projets "vedette" sont affichés (2 projets)
|
||||
- [] Chaque carte affiche : thumbnail, titre, badges
|
||||
- [] Cliquer sur une carte redirige vers `/projet/{slug}`
|
||||
- [] L'effet hover fonctionne (élévation, zoom image)
|
||||
- [] Responsive : 1 → 2 → 3 colonnes selon la taille
|
||||
- [] Images en lazy loading
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### File List
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `pages/projects.php` | Modified | Page liste projets vedettes |
|
||||
| `templates/project-card.php` | Created | Template carte projet réutilisable |
|
||||
| `assets/img/projects/default-project.svg` | Created | Placeholder image par défaut |
|
||||
|
||||
### Completion Notes
|
||||
- Grille responsive: 1 col (mobile) → 2 cols (sm) → 3 cols (lg)
|
||||
- Template project-card réutilisable avec badges (max 4 + compteur)
|
||||
- Lazy loading natif sur les images
|
||||
- Fallback onerror pour images manquantes
|
||||
- Message "Projets à venir" si aucun projet
|
||||
- 2 projets vedettes affichés correctement
|
||||
|
||||
### 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) |
|
||||
317
docs/stories/3.4.page-projet-individuelle.md
Normal file
317
docs/stories/3.4.page-projet-individuelle.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# Story 3.4: Page Projet Individuelle
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** visiteur,
|
||||
**I want** consulter une page dédiée pour chaque projet vedette,
|
||||
**so that** je comprends le contexte, la solution technique et vois le résultat.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. L'URL `/projet/{slug}` affiche le projet correspondant au slug
|
||||
2. Le slug est récupéré depuis le router (pas depuis $_GET)
|
||||
3. La page affiche les sections : Contexte, Solution technique, Travail d'équipe (si applicable), Durée, Témoignage (si disponible)
|
||||
4. Un bouton/lien permet de visiter le projet en ligne (ou affiche "Non disponible")
|
||||
5. Les technologies sont affichées sous forme de badges
|
||||
6. Des captures d'écran sont affichées si disponibles (galerie simple)
|
||||
7. Un lien "Retour aux projets" permet de revenir à la liste
|
||||
8. Si le slug n'existe pas, la page 404 est affichée
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Créer la page project-single.php** (AC: 1, 2, 8)
|
||||
- [] Créer `pages/project-single.php`
|
||||
- [] Récupérer le slug depuis `$GLOBALS['routeParams']`
|
||||
- [] Charger le projet avec `getProjectBySlug()`
|
||||
- [] Rediriger vers 404 si projet non trouvé
|
||||
|
||||
- [] **Task 2 : Afficher les informations principales** (AC: 3, 5)
|
||||
- [] Titre du projet
|
||||
- [] Badges technologies
|
||||
- [] Section Contexte
|
||||
- [] Section Solution technique
|
||||
- [] Section Travail d'équipe (si non null)
|
||||
- [] Durée du projet
|
||||
|
||||
- [] **Task 3 : Ajouter le lien vers le projet** (AC: 4)
|
||||
- [] Bouton "Voir le projet en ligne" si URL disponible
|
||||
- [] Bouton "Voir sur GitHub" si URL GitHub disponible
|
||||
- [] Message "Projet non disponible en ligne" si aucun lien
|
||||
|
||||
- [] **Task 4 : Afficher la galerie de captures** (AC: 6)
|
||||
- [] Grille de screenshots
|
||||
- [] Lazy loading sur les images
|
||||
- [ ] Lightbox optionnel (amélioration future)
|
||||
|
||||
- [] **Task 5 : Ajouter le témoignage** (AC: 3)
|
||||
- [] Placeholder préparé pour Story 4.5
|
||||
- [ ] Récupérer le témoignage lié au projet (Story 4.5)
|
||||
|
||||
- [] **Task 6 : Ajouter la navigation** (AC: 7)
|
||||
- [] Breadcrumb en haut de page
|
||||
- [] Lien "Retour aux projets"
|
||||
- [] CTA "Me contacter" en bas
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Page pages/project-single.php
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Page projet individuelle
|
||||
*/
|
||||
|
||||
// Récupérer le slug depuis le router
|
||||
$slug = $GLOBALS['routeParams'][0] ?? null;
|
||||
|
||||
if (!$slug) {
|
||||
http_response_code(404);
|
||||
include __DIR__ . '/404.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$project = getProjectBySlug($slug);
|
||||
|
||||
if (!$project) {
|
||||
http_response_code(404);
|
||||
include __DIR__ . '/404.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Récupérer le témoignage lié (si existe)
|
||||
$testimonial = getTestimonialByProject($slug);
|
||||
|
||||
$pageTitle = $project['title'];
|
||||
$pageDescription = $project['context'] ?? "Découvrez le projet {$project['title']}";
|
||||
$currentPage = 'projects';
|
||||
|
||||
include_template('header', compact('pageTitle', 'pageDescription'));
|
||||
include_template('navbar', compact('currentPage'));
|
||||
?>
|
||||
|
||||
<main>
|
||||
<article class="section">
|
||||
<div class="container-content">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="breadcrumb mb-8">
|
||||
<a href="/" class="breadcrumb-link">Accueil</a>
|
||||
<span class="text-text-muted">/</span>
|
||||
<a href="/projets" class="breadcrumb-link">Projets</a>
|
||||
<span class="text-text-muted">/</span>
|
||||
<span class="breadcrumb-current"><?= htmlspecialchars($project['title']) ?></span>
|
||||
</nav>
|
||||
|
||||
<!-- Header du projet -->
|
||||
<header class="mb-12">
|
||||
<h1 class="text-display mb-4"><?= htmlspecialchars($project['title']) ?></h1>
|
||||
|
||||
<!-- Technologies -->
|
||||
<div class="flex flex-wrap gap-2 mb-6">
|
||||
<?php foreach ($project['technologies'] ?? [] as $tech): ?>
|
||||
<span class="badge badge-primary"><?= htmlspecialchars($tech) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- Boutons d'action -->
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<?php if (!empty($project['url'])): ?>
|
||||
<a href="<?= htmlspecialchars($project['url']) ?>" target="_blank" rel="noopener" class="btn-primary">
|
||||
Voir le projet en ligne
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
||||
</svg>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($project['github'])): ?>
|
||||
<a href="<?= htmlspecialchars($project['github']) ?>" target="_blank" rel="noopener" class="btn-secondary">
|
||||
Voir sur GitHub
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (empty($project['url']) && empty($project['github'])): ?>
|
||||
<span class="text-text-muted italic">Projet non disponible en ligne</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Image principale -->
|
||||
<?php if (!empty($project['thumbnail'])): ?>
|
||||
<div class="mb-12 rounded-lg overflow-hidden">
|
||||
<img
|
||||
src="/assets/img/projects/<?= htmlspecialchars($project['thumbnail']) ?>"
|
||||
alt="<?= htmlspecialchars($project['title']) ?>"
|
||||
class="w-full"
|
||||
loading="lazy"
|
||||
>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Contenu -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-12">
|
||||
<!-- Colonne principale -->
|
||||
<div class="lg:col-span-2 space-y-10">
|
||||
<!-- Contexte -->
|
||||
<?php if (!empty($project['context'])): ?>
|
||||
<section>
|
||||
<h2 class="text-heading mb-4">Contexte</h2>
|
||||
<p class="text-text-secondary leading-relaxed">
|
||||
<?= nl2br(htmlspecialchars($project['context'])) ?>
|
||||
</p>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Solution technique -->
|
||||
<?php if (!empty($project['solution'])): ?>
|
||||
<section>
|
||||
<h2 class="text-heading mb-4">Solution Technique</h2>
|
||||
<p class="text-text-secondary leading-relaxed">
|
||||
<?= nl2br(htmlspecialchars($project['solution'])) ?>
|
||||
</p>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Travail d'équipe -->
|
||||
<?php if (!empty($project['teamwork'])): ?>
|
||||
<section>
|
||||
<h2 class="text-heading mb-4">Travail d'Équipe</h2>
|
||||
<p class="text-text-secondary leading-relaxed">
|
||||
<?= nl2br(htmlspecialchars($project['teamwork'])) ?>
|
||||
</p>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Galerie -->
|
||||
<?php if (!empty($project['screenshots'])): ?>
|
||||
<section>
|
||||
<h2 class="text-heading mb-4">Captures d'écran</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<?php foreach ($project['screenshots'] as $screenshot): ?>
|
||||
<img
|
||||
src="/assets/img/projects/<?= htmlspecialchars($screenshot) ?>"
|
||||
alt="Capture d'écran - <?= htmlspecialchars($project['title']) ?>"
|
||||
class="rounded-lg"
|
||||
loading="lazy"
|
||||
>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="space-y-6">
|
||||
<!-- Durée -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="text-sm font-medium text-text-muted mb-1">Durée du projet</h3>
|
||||
<p class="text-lg font-semibold"><?= htmlspecialchars($project['duration'] ?? 'Non spécifiée') ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Témoignage -->
|
||||
<?php if ($testimonial): ?>
|
||||
<div class="testimonial">
|
||||
<blockquote class="text-text-secondary italic mb-4">
|
||||
"<?= htmlspecialchars($testimonial['quote']) ?>"
|
||||
</blockquote>
|
||||
<footer>
|
||||
<p class="font-medium text-text-primary"><?= htmlspecialchars($testimonial['author_name']) ?></p>
|
||||
<p class="text-sm text-text-muted">
|
||||
<?= htmlspecialchars($testimonial['author_role']) ?>
|
||||
<?php if (!empty($testimonial['author_company'])): ?>
|
||||
- <?= htmlspecialchars($testimonial['author_company']) ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- Navigation bas de page -->
|
||||
<footer class="mt-16 pt-8 border-t border-border flex flex-wrap justify-between items-center gap-4">
|
||||
<a href="/projets" class="btn-ghost">
|
||||
← Retour aux projets
|
||||
</a>
|
||||
<a href="/contact" class="btn-primary">
|
||||
Me contacter
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<?php include_template('footer'); ?>
|
||||
```
|
||||
|
||||
### Structure de la Page
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Breadcrumb │
|
||||
├─────────────────────────────────────────┤
|
||||
│ TITRE DU PROJET │
|
||||
│ [Badge] [Badge] [Badge] │
|
||||
│ [Voir en ligne] [GitHub] │
|
||||
├─────────────────────────────────────────┤
|
||||
│ [Image principale] │
|
||||
├─────────────────────────────────────────┤
|
||||
│ CONTENU │ SIDEBAR │
|
||||
│ ───────────── │ │
|
||||
│ Contexte │ Durée │
|
||||
│ Solution technique │ │
|
||||
│ Travail d'équipe │ Témoignage │
|
||||
│ Captures d'écran │ │
|
||||
├─────────────────────────────────────────┤
|
||||
│ [← Retour] [Me contacter] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
- [] `/projet/ecommerce-xyz` affiche le bon projet
|
||||
- [] `/projet/inexistant` affiche la page 404
|
||||
- [] Toutes les sections s'affichent correctement
|
||||
- [] Le bouton "Voir en ligne" fonctionne (si URL)
|
||||
- [] Le bouton "Voir sur GitHub" fonctionne (si GitHub)
|
||||
- [] Le breadcrumb est navigable
|
||||
- [] Les images sont en lazy loading
|
||||
- [] La page est responsive
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### File List
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `pages/project-single.php` | Modified | Page projet individuelle complète |
|
||||
|
||||
### Completion Notes
|
||||
- Récupération slug via router ($GLOBALS['routeParams'])
|
||||
- Redirection 404 si projet non trouvé
|
||||
- Sections: Contexte, Solution technique, Travail d'équipe (conditionnel)
|
||||
- Boutons: Voir en ligne + GitHub avec icônes SVG
|
||||
- Galerie screenshots en grille 2 colonnes
|
||||
- Sidebar avec durée du projet
|
||||
- Breadcrumb accessible avec aria-label
|
||||
- Navigation: retour + CTA contact
|
||||
- Lazy loading + fallback onerror sur images
|
||||
- Témoignage: placeholder préparé pour Story 4.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) |
|
||||
169
docs/stories/3.5.projets-secondaires.md
Normal file
169
docs/stories/3.5.projets-secondaires.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# Story 3.5: Liste des Projets Secondaires
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** visiteur,
|
||||
**I want** voir une liste simplifiée des projets secondaires,
|
||||
**so that** je découvre l'étendue du travail sans page dédiée pour chaque.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Sur `/projets`, une section "Autres projets" liste les projets où category = "secondaire"
|
||||
2. Chaque projet secondaire affiche : titre, courte description (1 ligne), technologies
|
||||
3. Le format est compact (liste ou petites cartes)
|
||||
4. Les projets secondaires peuvent avoir un lien externe direct (optionnel)
|
||||
5. La section est visuellement distincte des projets vedettes (séparateur, titre de section)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Ajouter la section dans projects.php** (AC: 1, 5)
|
||||
- [] Récupérer les projets secondaires
|
||||
- [] Ajouter un titre de section "Autres projets"
|
||||
- [] Ajouter un séparateur visuel
|
||||
|
||||
- [] **Task 2 : Créer le template project-card-compact.php** (AC: 2, 3)
|
||||
- [] Format liste horizontale
|
||||
- [] Titre cliquable (si URL)
|
||||
- [] Description courte (truncate si nécessaire)
|
||||
- [] Badges technologies (3 max)
|
||||
|
||||
- [] **Task 3 : Gérer les liens** (AC: 4)
|
||||
- [] Si URL → lien externe (nouvel onglet)
|
||||
- [] Si pas d'URL → texte simple
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Section à ajouter dans pages/projects.php
|
||||
|
||||
```php
|
||||
<!-- Après la grille des projets vedettes -->
|
||||
|
||||
<?php if (!empty($secondaryProjects)): ?>
|
||||
<!-- Séparateur -->
|
||||
<hr class="border-border my-16">
|
||||
|
||||
<!-- Section projets secondaires -->
|
||||
<section>
|
||||
<h2 class="text-heading mb-8">Autres projets</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<?php foreach ($secondaryProjects as $project): ?>
|
||||
<?php include_template('project-card-compact', ['project' => $project]); ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
### Template templates/project-card-compact.php
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Carte projet compacte (projets secondaires)
|
||||
* @param array $project Données du projet
|
||||
*/
|
||||
|
||||
$title = $project['title'] ?? 'Sans titre';
|
||||
$context = $project['context'] ?? '';
|
||||
$url = $project['url'] ?? null;
|
||||
$technologies = $project['technologies'] ?? [];
|
||||
$maxTechs = 3;
|
||||
|
||||
// Tronquer la description à ~100 caractères
|
||||
$shortContext = strlen($context) > 100
|
||||
? substr($context, 0, 100) . '...'
|
||||
: $context;
|
||||
?>
|
||||
|
||||
<article class="card hover:border-border transition-colors">
|
||||
<div class="card-body flex flex-col sm:flex-row sm:items-center gap-4">
|
||||
<!-- Titre et description -->
|
||||
<div class="flex-grow">
|
||||
<?php if ($url): ?>
|
||||
<a href="<?= htmlspecialchars($url) ?>"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="text-lg font-semibold text-text-primary hover:text-primary transition-colors inline-flex items-center gap-2">
|
||||
<?= htmlspecialchars($title) ?>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
||||
</svg>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<h3 class="text-lg font-semibold text-text-primary">
|
||||
<?= htmlspecialchars($title) ?>
|
||||
</h3>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($shortContext): ?>
|
||||
<p class="text-text-secondary text-sm mt-1">
|
||||
<?= htmlspecialchars($shortContext) ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Technologies -->
|
||||
<div class="flex flex-wrap gap-2 sm:flex-shrink-0">
|
||||
<?php foreach (array_slice($technologies, 0, $maxTechs) as $tech): ?>
|
||||
<span class="badge text-xs"><?= htmlspecialchars($tech) ?></span>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if (count($technologies) > $maxTechs): ?>
|
||||
<span class="badge badge-muted text-xs">+<?= count($technologies) - $maxTechs ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
```
|
||||
|
||||
### Différences Vedettes vs Secondaires
|
||||
|
||||
| Aspect | Projets Vedettes | Projets Secondaires |
|
||||
|--------|------------------|---------------------|
|
||||
| Format | Carte avec image | Ligne compacte |
|
||||
| Image | Thumbnail | Aucune |
|
||||
| Lien | Page dédiée | Lien externe direct |
|
||||
| Description | Non affichée dans la liste | 1 ligne |
|
||||
| Technologies | 4 max | 3 max |
|
||||
|
||||
## Testing
|
||||
|
||||
- [] La section "Autres projets" apparaît sous les projets vedettes
|
||||
- [] Seuls les projets "secondaire" sont listés (1 projet)
|
||||
- [] Chaque projet affiche : titre, description courte, badges
|
||||
- [] Le titre est cliquable si URL disponible
|
||||
- [] Le lien s'ouvre dans un nouvel onglet (target="_blank")
|
||||
- [] Le design est distinct des projets vedettes (séparateur hr)
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### File List
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `pages/projects.php` | Modified | Ajout section projets secondaires |
|
||||
| `templates/project-card-compact.php` | Created | Template carte compacte |
|
||||
|
||||
### Completion Notes
|
||||
- Section "Autres projets" avec séparateur visuel (hr)
|
||||
- Template compact: titre + description tronquée (100 chars) + badges (3 max)
|
||||
- Lien externe avec icône SVG si URL disponible
|
||||
- rel="noopener" pour sécurité
|
||||
- 1 projet secondaire affiché: "Site Vitrine Restaurant"
|
||||
|
||||
### 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) |
|
||||
196
docs/stories/3.6.optimisation-images.md
Normal file
196
docs/stories/3.6.optimisation-images.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# Story 3.6: Optimisation des Images Projets
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** visiteur,
|
||||
**I want** que les images des projets se chargent rapidement,
|
||||
**so that** je n'attends pas et j'ai une expérience fluide.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Les images sont stockées dans `assets/img/projects/`
|
||||
2. Le lazy loading est activé sur toutes les images (attribut `loading="lazy"`)
|
||||
3. Les images ont des dimensions explicites (width/height) pour éviter le layout shift
|
||||
4. Le format WebP est utilisé avec fallback JPG/PNG via `<picture>`
|
||||
5. Les thumbnails sont redimensionnés (max 400px de large)
|
||||
6. Le score Lighthouse "Images" est vert (>90)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Organiser le dossier images** (AC: 1)
|
||||
- [] Créer `assets/img/projects/`
|
||||
- [] Définir la convention de nommage : `{slug}-{type}.{ext}`
|
||||
- [] Exemple : `ecommerce-xyz-thumb.webp`, `ecommerce-xyz-screen-1.webp`
|
||||
|
||||
- [] **Task 2 : Implémenter le lazy loading** (AC: 2)
|
||||
- [] Ajouter `loading="lazy"` sur toutes les images projets
|
||||
- [] Image principale above-the-fold sans lazy loading (false)
|
||||
|
||||
- [] **Task 3 : Ajouter les dimensions explicites** (AC: 3)
|
||||
- [] Définir les tailles standards : thumbnail (400x225), screenshot (800x450), hero (1200x675)
|
||||
- [] Ajouter `width` et `height` sur toutes les `<img>`
|
||||
|
||||
- [] **Task 4 : Implémenter WebP avec fallback** (AC: 4)
|
||||
- [] Utiliser `<picture>` avec `<source type="image/webp">`
|
||||
- [] Fallback vers JPG
|
||||
|
||||
- [] **Task 5 : Documenter les tailles recommandées** (AC: 5)
|
||||
- [] Thumbnails : 400x225, qualité 80%
|
||||
- [] Screenshots : 800x450, qualité 85%
|
||||
- [] Hero : 1200x675, qualité 85%
|
||||
- [] Documentation dans Dev Notes
|
||||
|
||||
- [ ] **Task 6 : Tester les performances** (AC: 6)
|
||||
- [ ] Audit Lighthouse sur la page projets (requiert images réelles)
|
||||
- [ ] Vérifier le score images > 90
|
||||
- [ ] Vérifier le CLS < 0.1
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Convention de Nommage des Images
|
||||
|
||||
```
|
||||
assets/img/projects/
|
||||
├── ecommerce-xyz-thumb.webp # Thumbnail (400x225)
|
||||
├── ecommerce-xyz-thumb.jpg # Fallback JPG
|
||||
├── ecommerce-xyz-screen-1.webp # Screenshot 1 (800x450)
|
||||
├── ecommerce-xyz-screen-1.jpg # Fallback
|
||||
├── ecommerce-xyz-screen-2.webp # Screenshot 2
|
||||
├── app-gestion-thumb.webp
|
||||
├── app-gestion-screen-1.webp
|
||||
└── default-project.webp # Image par défaut
|
||||
```
|
||||
|
||||
### Format : {slug}-{type}.{extension}
|
||||
|
||||
| Type | Usage | Dimensions | Qualité |
|
||||
|------|-------|------------|---------|
|
||||
| thumb | Carte projet | 400x225 (16:9) | 80% |
|
||||
| screen-N | Captures d'écran | 800x450 (16:9) | 85% |
|
||||
| hero | Image principale | 1200x675 (16:9) | 85% |
|
||||
|
||||
### Template Image avec Picture
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Helper pour afficher une image optimisée
|
||||
*/
|
||||
function projectImage(string $filename, string $alt, int $width, int $height, bool $lazy = true): string
|
||||
{
|
||||
$webp = $filename;
|
||||
$fallback = str_replace('.webp', '.jpg', $filename);
|
||||
$lazyAttr = $lazy ? 'loading="lazy"' : '';
|
||||
|
||||
return <<<HTML
|
||||
<picture>
|
||||
<source srcset="/assets/img/projects/{$webp}" type="image/webp">
|
||||
<img
|
||||
src="/assets/img/projects/{$fallback}"
|
||||
alt="{$alt}"
|
||||
width="{$width}"
|
||||
height="{$height}"
|
||||
{$lazyAttr}
|
||||
class="w-full h-full object-cover"
|
||||
>
|
||||
</picture>
|
||||
HTML;
|
||||
}
|
||||
```
|
||||
|
||||
### Usage dans les Templates
|
||||
|
||||
```php
|
||||
<!-- Dans project-card.php -->
|
||||
<div class="aspect-thumbnail overflow-hidden">
|
||||
<?= projectImage(
|
||||
$project['thumbnail'],
|
||||
"Aperçu du projet " . $project['title'],
|
||||
400,
|
||||
225,
|
||||
true // lazy loading
|
||||
) ?>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Checklist Optimisation Image
|
||||
|
||||
Pour chaque image :
|
||||
- [ ] Format WebP créé
|
||||
- [ ] Fallback JPG créé
|
||||
- [ ] Redimensionnée à la taille cible
|
||||
- [ ] Compressée (qualité 80-85%)
|
||||
- [ ] Nommée selon la convention
|
||||
|
||||
### Outils Recommandés
|
||||
|
||||
| Outil | Usage |
|
||||
|-------|-------|
|
||||
| Squoosh.app | Compression web (gratuit) |
|
||||
| ImageMagick | CLI batch conversion |
|
||||
| Sharp (Node) | Automatisation build |
|
||||
|
||||
### Commandes ImageMagick
|
||||
|
||||
```bash
|
||||
# Convertir en WebP
|
||||
convert input.jpg -quality 80 -resize 400x225 output.webp
|
||||
|
||||
# Batch conversion
|
||||
for f in *.jpg; do
|
||||
convert "$f" -quality 80 "${f%.jpg}.webp"
|
||||
done
|
||||
```
|
||||
|
||||
### Métriques de Performance Cibles
|
||||
|
||||
| Métrique | Objectif |
|
||||
|----------|----------|
|
||||
| Lighthouse Images | > 90 |
|
||||
| CLS | < 0.1 |
|
||||
| LCP (si image above-fold) | < 2.5s |
|
||||
| Taille thumbnail | < 30kb |
|
||||
| Taille screenshot | < 80kb |
|
||||
|
||||
## Testing
|
||||
|
||||
- [] Toutes les images sont dans `assets/img/projects/`
|
||||
- [] Le lazy loading fonctionne (attribut loading="lazy")
|
||||
- [] Dimensions explicites pour éviter CLS (width/height)
|
||||
- [] Les images WebP sont servies via `<picture>`
|
||||
- [] Le fallback JPG est présent dans `<img src>`
|
||||
- [ ] Score Lighthouse images > 90 (requiert images réelles)
|
||||
|
||||
## 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 fonction projectImage() |
|
||||
| `templates/project-card.php` | Modified | Utilise projectImage() |
|
||||
| `pages/project-single.php` | Modified | Utilise projectImage() pour hero et galerie |
|
||||
|
||||
### Completion Notes
|
||||
- Fonction `projectImage()` créée avec support `<picture>` WebP + fallback JPG
|
||||
- Tailles standards définies: thumbnail (400x225), screenshot (800x450), hero (1200x675)
|
||||
- Lazy loading activé par défaut, désactivé pour images above-the-fold
|
||||
- Fallback onerror vers default-project.svg
|
||||
- Templates mis à jour: project-card.php, project-single.php
|
||||
- Task 6 (Lighthouse) non testable sans images réelles
|
||||
|
||||
### 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) |
|
||||
211
docs/stories/4.1.page-competences-technologies.md
Normal file
211
docs/stories/4.1.page-competences-technologies.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# Story 4.1: Page Compétences - Technologies Liées aux Projets
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** visiteur,
|
||||
**I want** voir les compétences techniques du développeur liées aux projets réalisés,
|
||||
**so that** je vérifie qu'il maîtrise les technologies dont j'ai besoin.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. `/competences` affiche une liste des technologies de développement (HTML, CSS, JS, PHP, etc.)
|
||||
2. Chaque technologie est liée aux projets qui l'utilisent (liens cliquables)
|
||||
3. Les technologies sont groupées par catégorie (Frontend, Backend, Outils, etc.)
|
||||
4. Un indicateur visuel montre le nombre de projets utilisant chaque technologie
|
||||
5. Le design utilise des badges/tags cohérents avec les pages projets
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Créer la page skills.php** (AC: 1)
|
||||
- [] Mettre à jour `pages/skills.php`
|
||||
- [] Inclure header, navbar, footer
|
||||
- [] Route `/competences` déjà configurée
|
||||
|
||||
- [] **Task 2 : Créer la structure de données des technologies**
|
||||
- [] Définir les catégories : Frontend, Backend, Base de données, DevOps
|
||||
- [] Lister les technologies par catégorie
|
||||
- [] Comptage automatique via getProjectCountByTech()
|
||||
|
||||
- [] **Task 3 : Afficher les technologies groupées** (AC: 3)
|
||||
- [] Section par catégorie avec icône
|
||||
- [] Titre de catégorie
|
||||
- [] Liste des technologies
|
||||
|
||||
- [] **Task 4 : Lier aux projets** (AC: 2, 4)
|
||||
- [] Compter les projets par technologie
|
||||
- [] Afficher le compteur en badge
|
||||
- [] Tooltip avec nombre de projets
|
||||
|
||||
- [] **Task 5 : Styler avec les badges** (AC: 5)
|
||||
- [] Technologies avec projets: fond coloré + compteur
|
||||
- [] Technologies sans projet: grisées
|
||||
- [] Effet hover
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Page pages/skills.php
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Page Compétences
|
||||
*/
|
||||
|
||||
$pageTitle = 'Compétences';
|
||||
$pageDescription = 'Mes compétences techniques en développement web : langages, frameworks et outils.';
|
||||
$currentPage = 'skills';
|
||||
|
||||
// Récupérer toutes les technologies depuis les projets
|
||||
$projects = getProjects();
|
||||
$techCount = [];
|
||||
|
||||
foreach ($projects as $project) {
|
||||
foreach ($project['technologies'] ?? [] as $tech) {
|
||||
$techCount[$tech] = ($techCount[$tech] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Catégoriser les technologies
|
||||
$categories = [
|
||||
'Frontend' => ['HTML', 'CSS', 'JavaScript', 'TypeScript', 'React', 'Vue.js', 'Tailwind CSS', 'Bootstrap', 'SASS'],
|
||||
'Backend' => ['PHP', 'Node.js', 'Python', 'Laravel', 'Express', 'Symfony'],
|
||||
'Base de données' => ['MySQL', 'PostgreSQL', 'MongoDB', 'SQLite', 'Redis'],
|
||||
'DevOps & Outils' => ['Git', 'Docker', 'Linux', 'Nginx', 'Apache', 'CI/CD'],
|
||||
];
|
||||
|
||||
include_template('header', compact('pageTitle', 'pageDescription'));
|
||||
include_template('navbar', compact('currentPage'));
|
||||
?>
|
||||
|
||||
<main>
|
||||
<section class="section">
|
||||
<div class="container-content">
|
||||
<div class="section-header">
|
||||
<h1 class="section-title">Compétences</h1>
|
||||
<p class="section-subtitle">
|
||||
Technologies que j'utilise au quotidien, liées à mes projets réels.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Technologies par catégorie -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 lg:gap-12">
|
||||
<?php foreach ($categories as $category => $techs): ?>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h2 class="text-subheading mb-6"><?= htmlspecialchars($category) ?></h2>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<?php foreach ($techs as $tech): ?>
|
||||
<?php $count = $techCount[$tech] ?? 0; ?>
|
||||
<?php if ($count > 0): ?>
|
||||
<a href="/projets?tech=<?= urlencode($tech) ?>"
|
||||
class="group flex items-center gap-2 px-4 py-2 bg-surface-light rounded-lg hover:bg-primary/20 transition-colors">
|
||||
<span class="font-medium text-text-primary group-hover:text-primary">
|
||||
<?= htmlspecialchars($tech) ?>
|
||||
</span>
|
||||
<span class="text-xs px-2 py-0.5 bg-primary/20 text-primary rounded-full">
|
||||
<?= $count ?>
|
||||
</span>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="flex items-center gap-2 px-4 py-2 bg-surface-light/50 rounded-lg text-text-muted">
|
||||
<?= htmlspecialchars($tech) ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section outils (Story 4.2) sera ajoutée ici -->
|
||||
</main>
|
||||
|
||||
<?php include_template('footer'); ?>
|
||||
```
|
||||
|
||||
### Logique de Comptage
|
||||
|
||||
```php
|
||||
/**
|
||||
* Compte les projets par technologie
|
||||
*/
|
||||
function getProjectCountByTech(): array
|
||||
{
|
||||
$projects = getProjects();
|
||||
$count = [];
|
||||
|
||||
foreach ($projects as $project) {
|
||||
foreach ($project['technologies'] ?? [] as $tech) {
|
||||
$count[$tech] = ($count[$tech] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les projets utilisant une technologie
|
||||
*/
|
||||
function getProjectsByTech(string $tech): array
|
||||
{
|
||||
return array_filter(getProjects(), function($project) use ($tech) {
|
||||
return in_array($tech, $project['technologies'] ?? []);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Catégories de Technologies
|
||||
|
||||
| Catégorie | Technologies |
|
||||
|-----------|--------------|
|
||||
| Frontend | HTML, CSS, JS, React, Vue, Tailwind, etc. |
|
||||
| Backend | PHP, Node.js, Python, Laravel, etc. |
|
||||
| Base de données | MySQL, PostgreSQL, MongoDB, etc. |
|
||||
| DevOps & Outils | Git, Docker, Linux, CI/CD, etc. |
|
||||
|
||||
## Testing
|
||||
|
||||
- [] La page `/competences` s'affiche correctement
|
||||
- [] Les technologies sont groupées par catégorie
|
||||
- [] Le compteur de projets est affiché
|
||||
- [] Les technologies avec projets ont un tooltip
|
||||
- [] Les technologies sans projet sont grisées
|
||||
- [] Le design est cohérent avec le reste du site
|
||||
|
||||
## 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 getProjectCountByTech() et getProjectsByTech() |
|
||||
| `pages/skills.php` | Modified | Implémentation complète de la page compétences |
|
||||
|
||||
### Completion Notes
|
||||
- Page `/competences` avec 4 catégories de technologies (Frontend, Backend, Base de données, DevOps & Outils)
|
||||
- Icône SVG pour chaque catégorie
|
||||
- Compteur de projets affiché en badge pour chaque technologie
|
||||
- Tooltip avec nombre de projets au survol
|
||||
- Technologies sans projet associé affichées en grisé
|
||||
- Design cohérent avec les cartes du reste du site
|
||||
- Note: Les liens vers `/projets?tech=X` ont été retirés (filtrage à implémenter dans une future story)
|
||||
|
||||
### 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) |
|
||||
214
docs/stories/4.2.page-competences-outils.md
Normal file
214
docs/stories/4.2.page-competences-outils.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# Story 4.2: Page Compétences - Outils Démontrables
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** visiteur,
|
||||
**I want** voir les outils maîtrisés avec des preuves concrètes,
|
||||
**so that** je vérifie les compétences au-delà des simples affirmations.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Une section "Outils démontrables" liste les outils avec liens de preuve (Git → GitHub, Notion → page publique, etc.)
|
||||
2. Chaque outil a : nom, icône/logo, lien vers la preuve externe
|
||||
3. Une section "Autres outils" liste les outils non démontrables avec contexte d'utilisation
|
||||
4. Le design distingue clairement les deux types d'outils
|
||||
5. Les liens externes s'ouvrent dans un nouvel onglet
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Définir la structure des outils**
|
||||
- [] Créer un tableau d'outils démontrables avec liens
|
||||
- [] Créer un tableau d'autres outils avec contexte
|
||||
|
||||
- [] **Task 2 : Ajouter la section "Outils démontrables"** (AC: 1, 2)
|
||||
- [] Titre de section
|
||||
- [] Grille d'outils avec icône et lien
|
||||
- [] Effet hover
|
||||
|
||||
- [] **Task 3 : Ajouter la section "Autres outils"** (AC: 3)
|
||||
- [] Titre de section
|
||||
- [] Liste avec description du contexte
|
||||
- [] Style différent (moins mis en avant)
|
||||
|
||||
- [] **Task 4 : Implémenter les liens externes** (AC: 4, 5)
|
||||
- [] `target="_blank"` et `rel="noopener"`
|
||||
- [] Icône "lien externe" visuelle
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Données des Outils
|
||||
|
||||
```php
|
||||
// Outils démontrables (avec preuves)
|
||||
$demonstrableTools = [
|
||||
[
|
||||
'name' => 'Git / GitHub',
|
||||
'icon' => 'github',
|
||||
'url' => 'https://github.com/votre-username',
|
||||
'description' => 'Historique de commits et projets publics'
|
||||
],
|
||||
[
|
||||
'name' => 'VS Code',
|
||||
'icon' => 'vscode',
|
||||
'url' => null, // pas de lien mais démontrable via code
|
||||
'description' => 'Éditeur principal, configuration partagée'
|
||||
],
|
||||
[
|
||||
'name' => 'Figma',
|
||||
'icon' => 'figma',
|
||||
'url' => 'https://figma.com/@votre-username',
|
||||
'description' => 'Maquettes et prototypes'
|
||||
],
|
||||
[
|
||||
'name' => 'Notion',
|
||||
'icon' => 'notion',
|
||||
'url' => 'https://notion.so/votre-page-publique',
|
||||
'description' => 'Organisation et documentation'
|
||||
],
|
||||
[
|
||||
'name' => 'Docker',
|
||||
'icon' => 'docker',
|
||||
'url' => 'https://hub.docker.com/u/votre-username',
|
||||
'description' => 'Images et configurations'
|
||||
],
|
||||
];
|
||||
|
||||
// Autres outils (sans preuve directe)
|
||||
$otherTools = [
|
||||
['name' => 'Photoshop', 'context' => 'Retouche d\'images et création graphique'],
|
||||
['name' => 'Insomnia', 'context' => 'Test d\'APIs REST'],
|
||||
['name' => 'DBeaver', 'context' => 'Administration de bases de données'],
|
||||
['name' => 'FileZilla', 'context' => 'Transfert FTP/SFTP'],
|
||||
['name' => 'Trello', 'context' => 'Gestion de projet Kanban'],
|
||||
];
|
||||
```
|
||||
|
||||
### Section à ajouter dans pages/skills.php
|
||||
|
||||
```php
|
||||
<!-- Outils démontrables -->
|
||||
<section class="section bg-surface">
|
||||
<div class="container-content">
|
||||
<h2 class="text-heading mb-8">Outils Démontrables</h2>
|
||||
<p class="text-text-secondary mb-8">
|
||||
Ces outils sont accompagnés de preuves vérifiables.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<?php foreach ($demonstrableTools as $tool): ?>
|
||||
<a href="<?= htmlspecialchars($tool['url']) ?>"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="card-interactive group">
|
||||
<div class="card-body flex items-start gap-4">
|
||||
<!-- Icône -->
|
||||
<div class="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||
<span class="text-2xl text-primary">
|
||||
<?= getToolIcon($tool['icon']) ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Contenu -->
|
||||
<div class="flex-grow">
|
||||
<h3 class="font-semibold text-text-primary group-hover:text-primary transition-colors flex items-center gap-2">
|
||||
<?= htmlspecialchars($tool['name']) ?>
|
||||
<svg class="w-4 h-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
||||
</svg>
|
||||
</h3>
|
||||
<p class="text-sm text-text-muted mt-1">
|
||||
<?= htmlspecialchars($tool['description']) ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Autres outils -->
|
||||
<section class="section">
|
||||
<div class="container-content">
|
||||
<h2 class="text-heading mb-8">Autres Outils</h2>
|
||||
<p class="text-text-secondary mb-8">
|
||||
Outils utilisés régulièrement dans mes projets.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<?php foreach ($otherTools as $tool): ?>
|
||||
<div class="group relative">
|
||||
<span class="badge text-sm cursor-help">
|
||||
<?= htmlspecialchars($tool['name']) ?>
|
||||
</span>
|
||||
<!-- Tooltip -->
|
||||
<div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-surface-light text-text-secondary text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap">
|
||||
<?= htmlspecialchars($tool['context']) ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
### Icônes des Outils
|
||||
|
||||
Utiliser des SVG inline ou une sprite d'icônes pour les outils :
|
||||
|
||||
```php
|
||||
function getToolIcon(string $icon): string
|
||||
{
|
||||
$icons = [
|
||||
'github' => '<svg>...</svg>',
|
||||
'vscode' => '<svg>...</svg>',
|
||||
'figma' => '<svg>...</svg>',
|
||||
'notion' => '<svg>...</svg>',
|
||||
'docker' => '<svg>...</svg>',
|
||||
];
|
||||
|
||||
return $icons[$icon] ?? '🔧';
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
- [] La section "Outils démontrables" affiche les outils avec liens
|
||||
- [] Les liens s'ouvrent dans un nouvel onglet
|
||||
- [] L'icône "lien externe" est visible
|
||||
- [] La section "Autres outils" est visuellement distincte
|
||||
- [] Les tooltips fonctionnent au hover
|
||||
- [] Le design est responsive
|
||||
|
||||
## 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 fonction getToolIcon() avec SVG |
|
||||
| `pages/skills.php` | Modified | Ajout sections outils démontrables et autres outils |
|
||||
|
||||
### Completion Notes
|
||||
- Fonction `getToolIcon()` avec icônes SVG pour GitHub, VS Code, Figma, Docker, Linux
|
||||
- Section "Outils Démontrables" avec grille responsive (1→2→3 colonnes)
|
||||
- Liens externes avec `target="_blank"` et `rel="noopener"`
|
||||
- Icône lien externe SVG sur les outils avec URL
|
||||
- Section "Autres Outils" avec badges et tooltips au hover
|
||||
- Design distinct entre les deux sections (cartes vs badges)
|
||||
|
||||
### 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) |
|
||||
254
docs/stories/4.3.page-decouvrir-parcours.md
Normal file
254
docs/stories/4.3.page-decouvrir-parcours.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# Story 4.3: Page Me Découvrir - Parcours et Motivations
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** visiteur,
|
||||
**I want** en savoir plus sur le développeur en tant que personne,
|
||||
**so that** je crée une connexion humaine et évalue la compatibilité.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. `/a-propos` affiche les sections : Qui je suis, Mon parcours, Pourquoi ce métier
|
||||
2. Le ton est sympathique et authentique (pas trop formel, pas trop familier)
|
||||
3. Le texte est aéré avec des paragraphes courts
|
||||
4. Une photo professionnelle ou illustration est présente (optionnel mais recommandé)
|
||||
5. La localisation est mentionnée de façon générale (grande ville, pas adresse précise)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Créer la page about.php** (AC: 1)
|
||||
- [] Créer `pages/about.php`
|
||||
- [] Inclure header, navbar, footer
|
||||
- [] Configurer la route `/a-propos` (déjà fait en 3.2)
|
||||
|
||||
- [] **Task 2 : Créer la section "Qui je suis"** (AC: 2, 4, 5)
|
||||
- [] Photo ou illustration (placeholder SVG)
|
||||
- [] Texte d'introduction personnel
|
||||
- [] Localisation générale (Grand Est, France)
|
||||
|
||||
- [] **Task 3 : Créer la section "Mon parcours"** (AC: 2, 3)
|
||||
- [] Timeline ou liste des étapes clés (4 étapes)
|
||||
- [] Formation, expériences, projets marquants
|
||||
- [] Paragraphes courts et aérés
|
||||
|
||||
- [] **Task 4 : Créer la section "Pourquoi ce métier"** (AC: 2)
|
||||
- [] Motivations personnelles
|
||||
- [] Ce qui passionne dans le développement
|
||||
- [] Vision et valeurs
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Page pages/about.php
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Page Me Découvrir
|
||||
*/
|
||||
|
||||
$pageTitle = 'Me Découvrir';
|
||||
$pageDescription = 'Découvrez mon parcours, mes motivations et ce qui me passionne en tant que développeur web.';
|
||||
$currentPage = 'about';
|
||||
|
||||
include_template('header', compact('pageTitle', 'pageDescription'));
|
||||
include_template('navbar', compact('currentPage'));
|
||||
?>
|
||||
|
||||
<main>
|
||||
<!-- Section Hero / Qui je suis -->
|
||||
<section class="section">
|
||||
<div class="container-content">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
<!-- Photo -->
|
||||
<div class="order-2 lg:order-1">
|
||||
<div class="aspect-square max-w-md mx-auto lg:mx-0 rounded-2xl overflow-hidden bg-surface">
|
||||
<img
|
||||
src="/assets/img/profile.webp"
|
||||
alt="Photo de profil"
|
||||
class="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Texte -->
|
||||
<div class="order-1 lg:order-2">
|
||||
<h1 class="text-display mb-6">
|
||||
Bonjour, je suis <span class="text-primary">Prénom</span>
|
||||
</h1>
|
||||
|
||||
<p class="text-xl text-text-secondary mb-6 leading-relaxed">
|
||||
Développeur web passionné basé à <strong>Ville, France</strong>.
|
||||
Je crée des expériences numériques qui allient performance,
|
||||
accessibilité et design soigné.
|
||||
</p>
|
||||
|
||||
<p class="text-text-secondary leading-relaxed">
|
||||
Depuis X ans, je transforme des idées en solutions web concrètes.
|
||||
Mon approche : comprendre les besoins, proposer des solutions pragmatiques,
|
||||
et livrer un travail dont je suis fier.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Mon Parcours -->
|
||||
<section class="section bg-surface">
|
||||
<div class="container-content">
|
||||
<h2 class="text-heading mb-12 text-center">Mon Parcours</h2>
|
||||
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<!-- Timeline -->
|
||||
<div class="space-y-8">
|
||||
<!-- Étape 1 -->
|
||||
<div class="flex gap-6">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-full bg-primary/20 flex items-center justify-center">
|
||||
<span class="text-primary font-bold">1</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-2">Formation</h3>
|
||||
<p class="text-text-secondary">
|
||||
[Votre formation - école, diplôme, année]
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Étape 2 -->
|
||||
<div class="flex gap-6">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-full bg-primary/20 flex items-center justify-center">
|
||||
<span class="text-primary font-bold">2</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-2">Premières Expériences</h3>
|
||||
<p class="text-text-secondary">
|
||||
[Stages, premiers emplois, projets personnels]
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Étape 3 -->
|
||||
<div class="flex gap-6">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-full bg-primary/20 flex items-center justify-center">
|
||||
<span class="text-primary font-bold">3</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-2">Aujourd'hui</h3>
|
||||
<p class="text-text-secondary">
|
||||
[Situation actuelle, spécialisation, objectifs]
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Pourquoi ce métier -->
|
||||
<section class="section">
|
||||
<div class="container-content">
|
||||
<div class="max-w-3xl mx-auto text-center">
|
||||
<h2 class="text-heading mb-8">Pourquoi le Développement Web ?</h2>
|
||||
|
||||
<div class="space-y-6 text-text-secondary text-lg leading-relaxed">
|
||||
<p>
|
||||
Ce qui me passionne dans le développement, c'est la possibilité de
|
||||
<strong class="text-text-primary">créer quelque chose à partir de rien</strong>.
|
||||
Une idée, du code, et soudain un site web existe et aide des gens.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
J'aime particulièrement le challenge de rendre les choses
|
||||
<strong class="text-text-primary">simples pour l'utilisateur</strong>,
|
||||
même quand elles sont complexes sous le capot.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Mon objectif : livrer un travail dont je suis fier, avec des solutions
|
||||
qui durent dans le temps et qui sont agréables à utiliser.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="section bg-surface">
|
||||
<div class="container-content text-center">
|
||||
<h2 class="text-heading mb-4">Envie d'en savoir plus ?</h2>
|
||||
<p class="text-text-secondary mb-8">
|
||||
Découvrez mes réalisations ou contactez-moi directement.
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
<a href="/projets" class="btn-primary">Voir mes projets</a>
|
||||
<a href="/contact" class="btn-secondary">Me contacter</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<?php include_template('footer'); ?>
|
||||
```
|
||||
|
||||
### Ton Rédactionnel
|
||||
|
||||
| À faire | À éviter |
|
||||
|---------|----------|
|
||||
| Phrases courtes et directes | Jargon technique excessif |
|
||||
| Ton conversationnel | Ton trop corporate |
|
||||
| Anecdotes personnelles | Informations trop personnelles |
|
||||
| Montrer la passion | Arrogance ou fausse modestie |
|
||||
|
||||
### Contenu à Personnaliser
|
||||
|
||||
- [ ] Photo de profil professionnelle
|
||||
- [ ] Ville (pas d'adresse précise)
|
||||
- [ ] Années d'expérience
|
||||
- [ ] Formation et diplômes
|
||||
- [ ] Étapes clés du parcours
|
||||
- [ ] Motivations personnelles
|
||||
|
||||
## Testing
|
||||
|
||||
- [] La page `/a-propos` s'affiche correctement
|
||||
- [] Les 3 sections sont présentes (Qui je suis, Parcours, Pourquoi)
|
||||
- [] Le ton est approprié (sympathique, authentique)
|
||||
- [] Les paragraphes sont courts et aérés
|
||||
- [] Le placeholder photo s'affiche correctement
|
||||
- [] Les CTA en bas de page fonctionnent
|
||||
- [] La page est responsive
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### File List
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `pages/about.php` | Modified | Implémentation complète de la page Me Découvrir |
|
||||
|
||||
### Completion Notes
|
||||
- Section "Qui je suis" avec placeholder photo SVG (gradient + icône)
|
||||
- Prénom personnalisé : "Célian"
|
||||
- Localisation générale : "Grand Est, France"
|
||||
- Timeline de 4 étapes pour le parcours
|
||||
- L'étape "Aujourd'hui" est mise en avant (badge plein)
|
||||
- Section "Pourquoi le développement" centrée avec emphases
|
||||
- CTA vers /projets et /contact
|
||||
- Ton authentique et sympathique
|
||||
- Note: Le placeholder peut être remplacé par une vraie photo dans `/assets/img/profile.webp`
|
||||
|
||||
### 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) |
|
||||
187
docs/stories/4.4.page-decouvrir-passions.md
Normal file
187
docs/stories/4.4.page-decouvrir-passions.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# Story 4.4: Page Me Découvrir - Passions et Hobbies
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** visiteur,
|
||||
**I want** découvrir les passions du développeur en dehors du code,
|
||||
**so that** je vois la personne au-delà du professionnel.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Une section "En dehors du code" présente les hobbies et passions
|
||||
2. Des preuves visuelles sont incluses si possible (photos d'événements, créations, etc.)
|
||||
3. Le contenu reste professionnel (pas d'informations trop personnelles)
|
||||
4. Les projets personnels sont mentionnés comme preuve de passion implicite
|
||||
5. Le design intègre harmonieusement texte et visuels
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Ajouter la section dans about.php** (AC: 1)
|
||||
- [] Titre "En dehors du code" ou "Mes passions"
|
||||
- [] Sous-titre engageant
|
||||
|
||||
- [] **Task 2 : Lister les hobbies** (AC: 1, 3)
|
||||
- [] 3-4 passions maximum (3 passions)
|
||||
- [] Description courte pour chaque
|
||||
- [] Garder un ton professionnel
|
||||
|
||||
- [] **Task 3 : Ajouter des visuels** (AC: 2, 5)
|
||||
- [] Placeholders SVG avec gradients pour chaque passion
|
||||
- [] Grille responsive (1→2→3 colonnes)
|
||||
- [] Effet hover sur les cartes
|
||||
|
||||
- [] **Task 4 : Mentionner les projets personnels** (AC: 4)
|
||||
- [] Lien vers GitHub (https://github.com/skycel)
|
||||
- [] Carte dédiée aux projets open source
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Section à ajouter dans pages/about.php
|
||||
|
||||
```php
|
||||
<!-- En dehors du code -->
|
||||
<section class="section">
|
||||
<div class="container-content">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">En Dehors du Code</h2>
|
||||
<p class="section-subtitle">
|
||||
Parce qu'un développeur a aussi une vie en dehors de l'écran.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<!-- Passion 1 -->
|
||||
<div class="card group">
|
||||
<div class="aspect-video overflow-hidden">
|
||||
<img
|
||||
src="/assets/img/hobbies/passion-1.webp"
|
||||
alt="[Description de la passion]"
|
||||
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold mb-2">[Passion 1]</h3>
|
||||
<p class="text-text-secondary text-sm">
|
||||
[Description courte de cette passion et pourquoi elle compte pour vous]
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Passion 2 -->
|
||||
<div class="card group">
|
||||
<div class="aspect-video overflow-hidden">
|
||||
<img
|
||||
src="/assets/img/hobbies/passion-2.webp"
|
||||
alt="[Description de la passion]"
|
||||
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold mb-2">[Passion 2]</h3>
|
||||
<p class="text-text-secondary text-sm">
|
||||
[Description courte]
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Passion 3 / Projets personnels -->
|
||||
<div class="card group">
|
||||
<div class="aspect-video overflow-hidden bg-gradient-to-br from-primary/20 to-primary/5 flex items-center justify-center">
|
||||
<svg class="w-16 h-16 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold mb-2">Projets Open Source</h3>
|
||||
<p class="text-text-secondary text-sm mb-3">
|
||||
Je contribue à des projets open source et développe mes propres outils sur mon temps libre.
|
||||
</p>
|
||||
<a href="https://github.com/votre-username" target="_blank" rel="noopener" class="text-primary text-sm hover:underline inline-flex items-center gap-1">
|
||||
Voir sur GitHub
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
### Exemples de Passions Appropriées
|
||||
|
||||
| Passion | Pourquoi c'est bien | Ce qu'il faut éviter |
|
||||
|---------|---------------------|----------------------|
|
||||
| Sport (course, escalade) | Montre discipline et dépassement | Détails trop personnels |
|
||||
| Musique/instrument | Créativité, pratique régulière | Photos de soirées |
|
||||
| Voyages | Ouverture d'esprit, curiosité | Lieux trop identifiables |
|
||||
| Lecture/apprentissage | Curiosité intellectuelle | Opinions politiques |
|
||||
| Projets DIY/maker | Créativité technique | - |
|
||||
| Photographie | Sens esthétique | Photos personnelles |
|
||||
|
||||
### Images des Hobbies
|
||||
|
||||
```
|
||||
assets/img/hobbies/
|
||||
├── passion-1.webp # 400x225 (16:9)
|
||||
├── passion-2.webp
|
||||
├── passion-3.webp
|
||||
└── open-source.webp # Ou utiliser une icône
|
||||
```
|
||||
|
||||
### Équilibre Pro/Perso
|
||||
|
||||
Le contenu doit :
|
||||
- ✅ Humaniser le développeur
|
||||
- ✅ Montrer des qualités transférables (discipline, créativité)
|
||||
- ✅ Rester professionnel
|
||||
- ❌ Ne pas inclure de vie privée
|
||||
- ❌ Ne pas inclure d'opinions controversées
|
||||
- ❌ Ne pas en dire trop (3-4 passions max)
|
||||
|
||||
## Testing
|
||||
|
||||
- [] La section "En dehors du code" est présente
|
||||
- [] 3 passions sont affichées avec visuels (placeholders SVG)
|
||||
- [] Le contenu reste professionnel
|
||||
- [] Les placeholders sont légers (pas de lazy loading nécessaire)
|
||||
- [] Le lien GitHub fonctionne (target="_blank", rel="noopener")
|
||||
- [] La section est responsive (1→2→3 colonnes)
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### File List
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `pages/about.php` | Modified | Ajout section "En dehors du code" |
|
||||
|
||||
### Completion Notes
|
||||
- 3 cartes passion avec placeholders SVG et gradients colorés
|
||||
- Passion 1 : Musique (icône note, gradient purple/pink)
|
||||
- Passion 2 : Jeux vidéo (icône ticket, gradient green/cyan)
|
||||
- Passion 3 : Projets Open Source (icône code, gradient primary)
|
||||
- Lien GitHub vers https://github.com/skycel
|
||||
- Chaque carte a un effet hover sur le groupe
|
||||
- Design cohérent avec les cartes du reste du site
|
||||
- Ton professionnel : chaque passion est reliée à des compétences transférables
|
||||
- Note: Les placeholders peuvent être remplacés par des vraies photos dans `/assets/img/hobbies/`
|
||||
|
||||
### 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) |
|
||||
281
docs/stories/4.5.section-temoignages.md
Normal file
281
docs/stories/4.5.section-temoignages.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# Story 4.5: Section Témoignages (JSON Dynamique)
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** visiteur,
|
||||
**I want** lire des témoignages de clients ou employeurs,
|
||||
**so that** j'ai une preuve sociale de la qualité du travail.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Le fichier `data/testimonials.json` stocke tous les témoignages
|
||||
2. La structure JSON supporte : id, quote, author_name, author_role, author_company, author_photo, project_slug (optionnel), date, featured (booléen)
|
||||
3. Une fonction PHP `getTestimonials()` lit et décode le JSON
|
||||
4. La section témoignages affiche dynamiquement les entrées du JSON
|
||||
5. Chaque témoignage affiche : citation, nom, rôle/entreprise, photo (si disponible)
|
||||
6. Si un témoignage est lié à un projet (`project_slug`), un lien vers le projet est affiché
|
||||
7. Les témoignages `featured: true` peuvent être affichés sur la page d'accueil
|
||||
8. Si le JSON est vide ou le fichier absent, la section affiche "Témoignages à venir" ou est masquée
|
||||
9. Le design utilise des guillemets ou un style "citation" reconnaissable
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Créer le fichier testimonials.json** (AC: 1, 2)
|
||||
- [] Créer `data/testimonials.json`
|
||||
- [] Définir la structure complète
|
||||
- [] Ajouter 3 témoignages de test
|
||||
|
||||
- [] **Task 2 : Créer les fonctions PHP** (AC: 3)
|
||||
- [] `getTestimonials()` - tous les témoignages
|
||||
- [] `getFeaturedTestimonials()` - témoignages mis en avant
|
||||
- [] `getTestimonialByProject($slug)` - témoignage lié à un projet
|
||||
|
||||
- [] **Task 3 : Créer le template testimonial.php** (AC: 5, 9)
|
||||
- [] Style citation avec guillemets SVG
|
||||
- [] Photo de l'auteur (optionnelle, sinon initiale)
|
||||
- [] Nom, rôle, entreprise
|
||||
|
||||
- [] **Task 4 : Ajouter la section dans about.php** (AC: 4, 8)
|
||||
- [] Grille de témoignages (1→2→3 colonnes)
|
||||
- [] Gestion du cas vide (section masquée)
|
||||
|
||||
- [] **Task 5 : Lien vers le projet** (AC: 6)
|
||||
- [] Si project_slug existe, afficher le lien
|
||||
- [] "Voir le projet →" avec icône
|
||||
|
||||
- [] **Task 6 : Témoignages sur l'accueil** (AC: 7)
|
||||
- [] Afficher 2 témoignages featured sur home.php
|
||||
- [] Lien "Voir tous les témoignages"
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Structure data/testimonials.json
|
||||
|
||||
```json
|
||||
{
|
||||
"testimonials": [
|
||||
{
|
||||
"id": 1,
|
||||
"quote": "Excellent travail ! Le site a été livré dans les délais avec une qualité irréprochable. Communication fluide tout au long du projet.",
|
||||
"author_name": "Marie Dupont",
|
||||
"author_role": "Directrice Marketing",
|
||||
"author_company": "Entreprise XYZ",
|
||||
"author_photo": "marie-dupont.webp",
|
||||
"project_slug": "ecommerce-xyz",
|
||||
"date": "2025-06-15",
|
||||
"featured": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"quote": "Un développeur rigoureux et créatif. Il a su comprendre nos besoins et proposer des solutions adaptées.",
|
||||
"author_name": "Jean Martin",
|
||||
"author_role": "CEO",
|
||||
"author_company": "Startup ABC",
|
||||
"author_photo": null,
|
||||
"project_slug": "app-gestion",
|
||||
"date": "2025-03-20",
|
||||
"featured": true
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"quote": "Travail soigné et professionnel. Je recommande vivement.",
|
||||
"author_name": "Sophie Leroy",
|
||||
"author_role": "Gérante",
|
||||
"author_company": "Restaurant Le Bon Goût",
|
||||
"author_photo": null,
|
||||
"project_slug": null,
|
||||
"date": "2024-11-10",
|
||||
"featured": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Fonctions PHP (includes/functions.php)
|
||||
|
||||
```php
|
||||
/**
|
||||
* Récupère tous les témoignages
|
||||
*/
|
||||
function getTestimonials(): array
|
||||
{
|
||||
$data = loadJsonData('testimonials.json');
|
||||
return $data['testimonials'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les témoignages mis en avant
|
||||
*/
|
||||
function getFeaturedTestimonials(): array
|
||||
{
|
||||
return array_filter(getTestimonials(), fn($t) => $t['featured'] === true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le témoignage lié à un projet
|
||||
*/
|
||||
function getTestimonialByProject(string $projectSlug): ?array
|
||||
{
|
||||
$testimonials = getTestimonials();
|
||||
foreach ($testimonials as $testimonial) {
|
||||
if (($testimonial['project_slug'] ?? '') === $projectSlug) {
|
||||
return $testimonial;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
### Template templates/testimonial.php
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Composant témoignage
|
||||
* @param array $testimonial Données du témoignage
|
||||
* @param bool $showProjectLink Afficher le lien vers le projet
|
||||
*/
|
||||
|
||||
$quote = $testimonial['quote'] ?? '';
|
||||
$authorName = $testimonial['author_name'] ?? 'Anonyme';
|
||||
$authorRole = $testimonial['author_role'] ?? '';
|
||||
$authorCompany = $testimonial['author_company'] ?? '';
|
||||
$authorPhoto = $testimonial['author_photo'] ?? null;
|
||||
$projectSlug = $testimonial['project_slug'] ?? null;
|
||||
$showProjectLink = $showProjectLink ?? true;
|
||||
?>
|
||||
|
||||
<blockquote class="testimonial">
|
||||
<!-- Guillemets décoratifs -->
|
||||
<svg class="w-8 h-8 text-primary/30 mb-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z"/>
|
||||
</svg>
|
||||
|
||||
<!-- Citation -->
|
||||
<p class="text-text-primary text-lg leading-relaxed mb-6 italic">
|
||||
"<?= htmlspecialchars($quote) ?>"
|
||||
</p>
|
||||
|
||||
<!-- Auteur -->
|
||||
<footer class="flex items-center gap-4">
|
||||
<?php if ($authorPhoto): ?>
|
||||
<img
|
||||
src="/assets/img/testimonials/<?= htmlspecialchars($authorPhoto) ?>"
|
||||
alt="<?= htmlspecialchars($authorName) ?>"
|
||||
class="w-12 h-12 rounded-full object-cover"
|
||||
loading="lazy"
|
||||
>
|
||||
<?php else: ?>
|
||||
<div class="w-12 h-12 rounded-full bg-primary/20 flex items-center justify-center">
|
||||
<span class="text-primary font-semibold text-lg">
|
||||
<?= strtoupper(substr($authorName, 0, 1)) ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div>
|
||||
<p class="font-semibold text-text-primary"><?= htmlspecialchars($authorName) ?></p>
|
||||
<p class="text-sm text-text-muted">
|
||||
<?= htmlspecialchars($authorRole) ?>
|
||||
<?php if ($authorCompany): ?>
|
||||
<span class="text-text-muted">—</span> <?= htmlspecialchars($authorCompany) ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Lien vers le projet -->
|
||||
<?php if ($showProjectLink && $projectSlug): ?>
|
||||
<a href="/projet/<?= htmlspecialchars($projectSlug) ?>" class="inline-flex items-center gap-1 text-primary text-sm mt-4 hover:underline">
|
||||
Voir le projet
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</blockquote>
|
||||
```
|
||||
|
||||
### Section dans pages/about.php
|
||||
|
||||
```php
|
||||
<!-- Témoignages -->
|
||||
<?php $testimonials = getTestimonials(); ?>
|
||||
<?php if (!empty($testimonials)): ?>
|
||||
<section class="section bg-surface">
|
||||
<div class="container-content">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Ce Qu'ils Disent</h2>
|
||||
<p class="section-subtitle">
|
||||
Retours de clients et collaborateurs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<?php foreach ($testimonials as $testimonial): ?>
|
||||
<?php include_template('testimonial', ['testimonial' => $testimonial]); ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
### Dossier des Photos
|
||||
|
||||
```
|
||||
assets/img/testimonials/
|
||||
├── marie-dupont.webp # 96x96 carré
|
||||
├── jean-martin.webp
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
- [] Le fichier JSON est valide (3 témoignages)
|
||||
- [] `getTestimonials()` retourne les témoignages (3)
|
||||
- [] `getFeaturedTestimonials()` filtre correctement (2 featured)
|
||||
- [] La section s'affiche sur la page À propos
|
||||
- [] Les guillemets SVG et style citation sont visibles
|
||||
- [] Les initiales s'affichent si pas de photo
|
||||
- [] Le lien vers le projet fonctionne
|
||||
- [] Cas vide : section masquée (if !empty)
|
||||
- [] 2 témoignages featured affichés sur la home
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### File List
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `data/testimonials.json` | Created | 3 témoignages de test |
|
||||
| `includes/functions.php` | Modified | Fonctions getTestimonials(), getFeaturedTestimonials(), getTestimonialByProject() |
|
||||
| `templates/testimonial.php` | Created | Template avec guillemets, auteur, lien projet |
|
||||
| `pages/about.php` | Modified | Section "Ce Qu'ils Disent" |
|
||||
| `pages/home.php` | Modified | 2 témoignages featured |
|
||||
|
||||
### Completion Notes
|
||||
- Structure JSON complète : id, quote, author_name, author_role, author_company, author_photo, project_slug, date, featured
|
||||
- 3 fonctions PHP pour accéder aux témoignages
|
||||
- Template réutilisable avec guillemets SVG décoratifs
|
||||
- Photo optionnelle : si absente, affiche l'initiale sur fond coloré
|
||||
- Lien vers projet optionnel (paramètre showProjectLink)
|
||||
- Section masquée si JSON vide
|
||||
- 2 témoignages featured sur la home avec lien "Voir tous"
|
||||
- Note: Les photos peuvent être ajoutées dans `/assets/img/testimonials/`
|
||||
|
||||
### 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) |
|
||||
326
docs/stories/5.1.formulaire-structure-html5.md
Normal file
326
docs/stories/5.1.formulaire-structure-html5.md
Normal file
@@ -0,0 +1,326 @@
|
||||
# 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
|
||||
<?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) |
|
||||
351
docs/stories/5.2.validation-javascript.md
Normal file
351
docs/stories/5.2.validation-javascript.md
Normal file
@@ -0,0 +1,351 @@
|
||||
# Story 5.2: Validation JavaScript Côté Client
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** visiteur,
|
||||
**I want** être informé immédiatement si je fais une erreur de saisie,
|
||||
**so that** je corrige avant d'envoyer et j'évite les allers-retours.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. La validation JavaScript s'exécute à la soumission ET à la perte de focus (blur)
|
||||
2. Les messages d'erreur sont affichés sous chaque champ concerné
|
||||
3. Les champs en erreur sont visuellement distingués (bordure rouge, icône)
|
||||
4. Le message d'erreur est clair et indique comment corriger
|
||||
5. Le bouton d'envoi est désactivé tant que le formulaire contient des erreurs
|
||||
6. La validation est en JavaScript vanilla (pas de bibliothèque)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Créer le validateur de formulaire** (AC: 6)
|
||||
- [] Créer `assets/js/contact-form.js`
|
||||
- [] Classe ou objet `FormValidator`
|
||||
- [] Méthodes de validation par type de champ
|
||||
|
||||
- [] **Task 2 : Implémenter la validation au blur** (AC: 1)
|
||||
- [] Écouter l'événement `blur` sur chaque champ
|
||||
- [] Valider le champ concerné
|
||||
- [] Afficher/masquer l'erreur
|
||||
|
||||
- [] **Task 3 : Implémenter la validation à la soumission** (AC: 1)
|
||||
- [] Écouter l'événement `submit`
|
||||
- [] Valider tous les champs
|
||||
- [] Empêcher l'envoi si erreurs
|
||||
|
||||
- [] **Task 4 : Afficher les erreurs** (AC: 2, 3, 4)
|
||||
- [] Message sous le champ (data-error)
|
||||
- [] Bordure rouge sur le champ (classes Tailwind)
|
||||
- [] Messages clairs et actionnables
|
||||
|
||||
- [] **Task 5 : Gérer l'état du bouton** (AC: 5)
|
||||
- [] Désactiver si erreurs
|
||||
- [] Réactiver quand tout est valide
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Structure assets/js/contact-form.js
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Validation du formulaire de contact
|
||||
* JavaScript vanilla - pas de dépendances
|
||||
*/
|
||||
|
||||
class FormValidator {
|
||||
constructor(formId) {
|
||||
this.form = document.getElementById(formId);
|
||||
if (!this.form) return;
|
||||
|
||||
this.submitBtn = document.getElementById('submit-btn');
|
||||
this.fields = {};
|
||||
this.errors = {};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Définir les règles de validation
|
||||
this.rules = {
|
||||
nom: {
|
||||
required: true,
|
||||
minLength: 2,
|
||||
maxLength: 100,
|
||||
message: 'Veuillez entrer votre nom (2 caractères minimum)'
|
||||
},
|
||||
prenom: {
|
||||
required: true,
|
||||
minLength: 2,
|
||||
maxLength: 100,
|
||||
message: 'Veuillez entrer votre prénom (2 caractères minimum)'
|
||||
},
|
||||
email: {
|
||||
required: true,
|
||||
email: true,
|
||||
message: 'Veuillez entrer une adresse email valide'
|
||||
},
|
||||
categorie: {
|
||||
required: true,
|
||||
message: 'Veuillez sélectionner une catégorie'
|
||||
},
|
||||
objet: {
|
||||
required: true,
|
||||
minLength: 5,
|
||||
maxLength: 200,
|
||||
message: 'Veuillez entrer un objet (5 caractères minimum)'
|
||||
},
|
||||
message: {
|
||||
required: true,
|
||||
minLength: 20,
|
||||
maxLength: 5000,
|
||||
message: 'Veuillez entrer votre message (20 caractères minimum)'
|
||||
}
|
||||
};
|
||||
|
||||
// Récupérer les champs
|
||||
Object.keys(this.rules).forEach(fieldName => {
|
||||
this.fields[fieldName] = this.form.querySelector(`[name="${fieldName}"]`);
|
||||
});
|
||||
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Validation au blur
|
||||
Object.keys(this.fields).forEach(fieldName => {
|
||||
const field = this.fields[fieldName];
|
||||
if (field) {
|
||||
field.addEventListener('blur', () => this.validateField(fieldName));
|
||||
field.addEventListener('input', () => this.clearError(fieldName));
|
||||
}
|
||||
});
|
||||
|
||||
// Validation à la soumission
|
||||
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
|
||||
|
||||
// Compteur de caractères pour le message
|
||||
const messageField = this.fields.message;
|
||||
if (messageField) {
|
||||
messageField.addEventListener('input', () => this.updateCharCount());
|
||||
}
|
||||
}
|
||||
|
||||
validateField(fieldName) {
|
||||
const field = this.fields[fieldName];
|
||||
const rule = this.rules[fieldName];
|
||||
|
||||
if (!field || !rule) return true;
|
||||
|
||||
const value = field.value.trim();
|
||||
let isValid = true;
|
||||
let errorMessage = '';
|
||||
|
||||
// Required
|
||||
if (rule.required && !value) {
|
||||
isValid = false;
|
||||
errorMessage = rule.message;
|
||||
}
|
||||
|
||||
// Min length
|
||||
if (isValid && rule.minLength && value.length < rule.minLength) {
|
||||
isValid = false;
|
||||
errorMessage = rule.message;
|
||||
}
|
||||
|
||||
// Max length
|
||||
if (isValid && rule.maxLength && value.length > rule.maxLength) {
|
||||
isValid = false;
|
||||
errorMessage = `Maximum ${rule.maxLength} caractères`;
|
||||
}
|
||||
|
||||
// Email format
|
||||
if (isValid && rule.email && value) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(value)) {
|
||||
isValid = false;
|
||||
errorMessage = rule.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Afficher ou masquer l'erreur
|
||||
if (isValid) {
|
||||
this.clearError(fieldName);
|
||||
} else {
|
||||
this.showError(fieldName, errorMessage);
|
||||
}
|
||||
|
||||
this.errors[fieldName] = !isValid;
|
||||
this.updateSubmitButton();
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
validateAll() {
|
||||
let allValid = true;
|
||||
|
||||
Object.keys(this.rules).forEach(fieldName => {
|
||||
if (!this.validateField(fieldName)) {
|
||||
allValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
return allValid;
|
||||
}
|
||||
|
||||
showError(fieldName, message) {
|
||||
const field = this.fields[fieldName];
|
||||
const errorEl = this.form.querySelector(`[data-error="${fieldName}"]`);
|
||||
|
||||
if (field) {
|
||||
field.classList.add('input-error');
|
||||
field.setAttribute('aria-invalid', 'true');
|
||||
}
|
||||
|
||||
if (errorEl) {
|
||||
errorEl.textContent = message;
|
||||
errorEl.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
clearError(fieldName) {
|
||||
const field = this.fields[fieldName];
|
||||
const errorEl = this.form.querySelector(`[data-error="${fieldName}"]`);
|
||||
|
||||
if (field) {
|
||||
field.classList.remove('input-error');
|
||||
field.removeAttribute('aria-invalid');
|
||||
}
|
||||
|
||||
if (errorEl) {
|
||||
errorEl.textContent = '';
|
||||
errorEl.classList.add('hidden');
|
||||
}
|
||||
|
||||
this.errors[fieldName] = false;
|
||||
this.updateSubmitButton();
|
||||
}
|
||||
|
||||
updateSubmitButton() {
|
||||
const hasErrors = Object.values(this.errors).some(err => err);
|
||||
|
||||
if (this.submitBtn) {
|
||||
this.submitBtn.disabled = hasErrors;
|
||||
}
|
||||
}
|
||||
|
||||
updateCharCount() {
|
||||
const messageField = this.fields.message;
|
||||
const countEl = document.getElementById('message-count');
|
||||
|
||||
if (messageField && countEl) {
|
||||
countEl.textContent = messageField.value.length;
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.validateAll()) {
|
||||
// Focus sur le premier champ en erreur
|
||||
const firstError = Object.keys(this.errors).find(key => this.errors[key]);
|
||||
if (firstError && this.fields[firstError]) {
|
||||
this.fields[firstError].focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Si valide, déclencher l'envoi (géré par une autre partie du code)
|
||||
this.form.dispatchEvent(new CustomEvent('validSubmit'));
|
||||
}
|
||||
|
||||
getFormData() {
|
||||
const formData = {};
|
||||
Object.keys(this.fields).forEach(fieldName => {
|
||||
if (this.fields[fieldName]) {
|
||||
formData[fieldName] = this.fields[fieldName].value.trim();
|
||||
}
|
||||
});
|
||||
// Ajouter le champ entreprise (optionnel)
|
||||
const entreprise = this.form.querySelector('[name="entreprise"]');
|
||||
if (entreprise) {
|
||||
formData.entreprise = entreprise.value.trim();
|
||||
}
|
||||
return formData;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisation
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.contactFormValidator = new FormValidator('contact-form');
|
||||
});
|
||||
```
|
||||
|
||||
### Messages d'Erreur
|
||||
|
||||
| Champ | Message |
|
||||
|-------|---------|
|
||||
| nom | Veuillez entrer votre nom (2 caractères minimum) |
|
||||
| prenom | Veuillez entrer votre prénom (2 caractères minimum) |
|
||||
| email | Veuillez entrer une adresse email valide |
|
||||
| categorie | Veuillez sélectionner une catégorie |
|
||||
| objet | Veuillez entrer un objet (5 caractères minimum) |
|
||||
| message | Veuillez entrer votre message (20 caractères minimum) |
|
||||
|
||||
### Règles de Validation
|
||||
|
||||
| Champ | Required | Min | Max | Format |
|
||||
|-------|----------|-----|-----|--------|
|
||||
| nom | Oui | 2 | 100 | - |
|
||||
| prenom | Oui | 2 | 100 | - |
|
||||
| email | Oui | - | 255 | email |
|
||||
| entreprise | Non | - | 200 | - |
|
||||
| categorie | Oui | - | - | - |
|
||||
| objet | Oui | 5 | 200 | - |
|
||||
| message | Oui | 20 | 5000 | - |
|
||||
|
||||
## Testing
|
||||
|
||||
- [] La validation se déclenche au blur
|
||||
- [] La validation se déclenche à la soumission
|
||||
- [] Les messages d'erreur s'affichent sous les champs
|
||||
- [] Les champs en erreur ont une bordure rouge
|
||||
- [] Le bouton est désactivé si erreurs
|
||||
- [] Le compteur de caractères fonctionne
|
||||
- [] Le focus va au premier champ en erreur
|
||||
- [] Email invalide est détecté
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### File List
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `assets/js/contact-form.js` | Created | Classe FormValidator avec validation complète |
|
||||
| `pages/contact.php` | Modified | Lien vers le script JS, classes Tailwind pour erreurs |
|
||||
|
||||
### Completion Notes
|
||||
- Classe FormValidator en JavaScript vanilla (pas de dépendances)
|
||||
- Validation au blur et à la soumission
|
||||
- Messages d'erreur sous chaque champ avec data-error
|
||||
- Bordure rouge sur les champs invalides (classes Tailwind)
|
||||
- Bouton submit désactivé si erreurs (updateSubmitButton)
|
||||
- Compteur de caractères en temps réel
|
||||
- Focus automatique sur le premier champ en erreur
|
||||
- Validation email avec regex
|
||||
- Événement 'validSubmit' dispatché quand tout est valide
|
||||
- Gestion du reset du formulaire
|
||||
|
||||
### 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) |
|
||||
300
docs/stories/5.3.persistance-localstorage.md
Normal file
300
docs/stories/5.3.persistance-localstorage.md
Normal file
@@ -0,0 +1,300 @@
|
||||
# Story 5.3: Persistance des Données avec localStorage
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** visiteur,
|
||||
**I want** que mes données soient sauvegardées si je quitte la page,
|
||||
**so that** je ne ressaisisse pas tout si je reviens plus tard.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Chaque modification d'un champ sauvegarde automatiquement dans localStorage
|
||||
2. Au chargement de la page, les champs sont pré-remplis avec les données sauvegardées
|
||||
3. Le localStorage est vidé après un envoi réussi du formulaire
|
||||
4. Un bouton "Effacer le formulaire" permet de réinitialiser manuellement
|
||||
5. Les données sensibles (si ajoutées plus tard) ne sont PAS stockées
|
||||
6. Le stockage utilise une clé unique (ex: `portfolio_contact_form`)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Créer le gestionnaire de stockage** (AC: 6)
|
||||
- [] Clé unique `portfolio_contact_form`
|
||||
- [] Méthodes save, load, clear
|
||||
- [] Gestion des erreurs (localStorage indisponible)
|
||||
|
||||
- [] **Task 2 : Sauvegarder automatiquement** (AC: 1)
|
||||
- [] Écouter l'événement `input` sur chaque champ
|
||||
- [] Debounce pour éviter trop d'écritures (500ms)
|
||||
- [] Sauvegarder l'état complet
|
||||
|
||||
- [] **Task 3 : Restaurer au chargement** (AC: 2)
|
||||
- [] Charger les données au DOMContentLoaded
|
||||
- [] Pré-remplir chaque champ
|
||||
- [] Mettre à jour le compteur de caractères
|
||||
|
||||
- [] **Task 4 : Vider après envoi réussi** (AC: 3)
|
||||
- [] Appeler clear() après succès (événement formSuccess)
|
||||
- [] Réinitialiser le formulaire
|
||||
|
||||
- [] **Task 5 : Bouton "Effacer"** (AC: 4)
|
||||
- [] Écouter le clic sur le bouton (id="clear-form-btn")
|
||||
- [] Vider le localStorage
|
||||
- [] Réinitialiser le formulaire
|
||||
- [] Confirmation avec confirm()
|
||||
|
||||
- [] **Task 6 : Exclure les données sensibles** (AC: 5)
|
||||
- [] Ne pas stocker csrf_token, password, recaptcha_token
|
||||
- [] Documenté dans EXCLUDED_FIELDS
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Gestionnaire de Stockage (state.js)
|
||||
|
||||
```javascript
|
||||
// assets/js/state.js
|
||||
|
||||
/**
|
||||
* Gestionnaire d'état pour le localStorage
|
||||
*/
|
||||
const AppState = {
|
||||
STORAGE_KEY: 'portfolio_contact_form',
|
||||
|
||||
// Champs à ne jamais stocker
|
||||
EXCLUDED_FIELDS: ['csrf_token', 'password', 'recaptcha_token'],
|
||||
|
||||
/**
|
||||
* Vérifie si localStorage est disponible
|
||||
*/
|
||||
isStorageAvailable() {
|
||||
try {
|
||||
const test = '__storage_test__';
|
||||
localStorage.setItem(test, test);
|
||||
localStorage.removeItem(test);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sauvegarde les données du formulaire
|
||||
*/
|
||||
saveFormData(data) {
|
||||
if (!this.isStorageAvailable()) return;
|
||||
|
||||
try {
|
||||
// Filtrer les champs exclus
|
||||
const filteredData = {};
|
||||
Object.keys(data).forEach(key => {
|
||||
if (!this.EXCLUDED_FIELDS.includes(key)) {
|
||||
filteredData[key] = data[key];
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(filteredData));
|
||||
} catch (e) {
|
||||
console.warn('Impossible de sauvegarder dans localStorage:', e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Charge les données sauvegardées
|
||||
*/
|
||||
getFormData() {
|
||||
if (!this.isStorageAvailable()) return null;
|
||||
|
||||
try {
|
||||
const data = localStorage.getItem(this.STORAGE_KEY);
|
||||
return data ? JSON.parse(data) : null;
|
||||
} catch (e) {
|
||||
console.warn('Impossible de charger depuis localStorage:', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Efface les données sauvegardées
|
||||
*/
|
||||
clearFormData() {
|
||||
if (!this.isStorageAvailable()) return;
|
||||
|
||||
try {
|
||||
localStorage.removeItem(this.STORAGE_KEY);
|
||||
} catch (e) {
|
||||
// Silencieux
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Intégration dans contact-form.js
|
||||
|
||||
```javascript
|
||||
// Ajouter dans la classe FormValidator ou en complément
|
||||
|
||||
class ContactFormPersistence {
|
||||
constructor(formId) {
|
||||
this.form = document.getElementById(formId);
|
||||
if (!this.form) return;
|
||||
|
||||
this.debounceTimer = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.loadSavedData();
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Sauvegarder à chaque modification (avec debounce)
|
||||
this.form.addEventListener('input', () => {
|
||||
clearTimeout(this.debounceTimer);
|
||||
this.debounceTimer = setTimeout(() => this.saveData(), 500);
|
||||
});
|
||||
|
||||
// Bouton effacer
|
||||
const clearBtn = document.getElementById('clear-form-btn');
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => this.clearForm());
|
||||
}
|
||||
|
||||
// Écouter l'envoi réussi
|
||||
this.form.addEventListener('formSuccess', () => {
|
||||
AppState.clearFormData();
|
||||
});
|
||||
}
|
||||
|
||||
saveData() {
|
||||
const formData = new FormData(this.form);
|
||||
const data = {};
|
||||
|
||||
formData.forEach((value, key) => {
|
||||
data[key] = value;
|
||||
});
|
||||
|
||||
AppState.saveFormData(data);
|
||||
}
|
||||
|
||||
loadSavedData() {
|
||||
const savedData = AppState.getFormData();
|
||||
if (!savedData) return;
|
||||
|
||||
Object.keys(savedData).forEach(key => {
|
||||
const field = this.form.querySelector(`[name="${key}"]`);
|
||||
if (field && savedData[key]) {
|
||||
field.value = savedData[key];
|
||||
}
|
||||
});
|
||||
|
||||
// Mettre à jour le compteur de caractères
|
||||
const messageField = this.form.querySelector('[name="message"]');
|
||||
const countEl = document.getElementById('message-count');
|
||||
if (messageField && countEl) {
|
||||
countEl.textContent = messageField.value.length;
|
||||
}
|
||||
}
|
||||
|
||||
clearForm() {
|
||||
// Confirmation optionnelle
|
||||
if (!confirm('Êtes-vous sûr de vouloir effacer le formulaire ?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Vider le localStorage
|
||||
AppState.clearFormData();
|
||||
|
||||
// Réinitialiser le formulaire
|
||||
this.form.reset();
|
||||
|
||||
// Réinitialiser le compteur
|
||||
const countEl = document.getElementById('message-count');
|
||||
if (countEl) {
|
||||
countEl.textContent = '0';
|
||||
}
|
||||
|
||||
// Effacer les erreurs visuelles
|
||||
this.form.querySelectorAll('.input-error').forEach(el => {
|
||||
el.classList.remove('input-error');
|
||||
});
|
||||
this.form.querySelectorAll('[data-error]').forEach(el => {
|
||||
el.classList.add('hidden');
|
||||
el.textContent = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisation
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.contactFormPersistence = new ContactFormPersistence('contact-form');
|
||||
});
|
||||
```
|
||||
|
||||
### Clé de Stockage
|
||||
|
||||
```
|
||||
localStorage key: "portfolio_contact_form"
|
||||
|
||||
Exemple de données stockées:
|
||||
{
|
||||
"nom": "Dupont",
|
||||
"prenom": "Marie",
|
||||
"email": "marie@example.com",
|
||||
"entreprise": "Acme Corp",
|
||||
"categorie": "projet",
|
||||
"objet": "Nouveau site web",
|
||||
"message": "Bonjour, je souhaite..."
|
||||
}
|
||||
```
|
||||
|
||||
### Champs Exclus du Stockage
|
||||
|
||||
- `csrf_token` (sécurité)
|
||||
- `password` (si ajouté)
|
||||
- `recaptcha_token` (généré dynamiquement)
|
||||
|
||||
## Testing
|
||||
|
||||
- [] Les données sont sauvegardées à la saisie
|
||||
- [] Les données sont restaurées au rechargement
|
||||
- [] Le bouton "Effacer" vide le formulaire
|
||||
- [] Le localStorage est vidé après envoi réussi (événement formSuccess)
|
||||
- [] Les champs exclus ne sont pas stockés (EXCLUDED_FIELDS)
|
||||
- [] Pas d'erreur si localStorage indisponible (isStorageAvailable)
|
||||
- [] Le compteur de caractères est mis à jour au chargement
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### File List
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `assets/js/state.js` | Created | Objet AppState pour gestion localStorage |
|
||||
| `assets/js/contact-form.js` | Modified | Ajout classe ContactFormPersistence |
|
||||
| `pages/contact.php` | Modified | Bouton Effacer + script state.js |
|
||||
|
||||
### Completion Notes
|
||||
- AppState avec clé unique `portfolio_contact_form`
|
||||
- Vérification disponibilité localStorage (try/catch)
|
||||
- Filtrage des champs sensibles (csrf_token, password, recaptcha_token)
|
||||
- Debounce de 500ms sur la sauvegarde
|
||||
- Restauration automatique au chargement de la page
|
||||
- Bouton "Effacer" avec confirmation et reset complet
|
||||
- Événement `formSuccess` pour vider après envoi réussi
|
||||
- Scripts chargés avec defer pour ne pas bloquer le rendu
|
||||
|
||||
### 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) |
|
||||
254
docs/stories/5.4.integration-recaptcha.md
Normal file
254
docs/stories/5.4.integration-recaptcha.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# Story 5.4: Intégration reCAPTCHA v3
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** propriétaire du site,
|
||||
**I want** une protection anti-spam invisible,
|
||||
**so that** je ne reçois pas de spam sans pénaliser l'expérience utilisateur.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. reCAPTCHA v3 est intégré (invisible, pas de case à cocher)
|
||||
2. Le script reCAPTCHA est chargé depuis Google
|
||||
3. Un token est généré à la soumission du formulaire
|
||||
4. Le token est envoyé avec les données du formulaire au backend PHP
|
||||
5. Les clés API (site key) sont configurables (fichier de config ou .env)
|
||||
6. Si reCAPTCHA échoue à charger, le formulaire reste utilisable (dégradation gracieuse)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Configurer les clés reCAPTCHA** (AC: 5)
|
||||
- [] Ajouter RECAPTCHA_SITE_KEY dans .env
|
||||
- [] Ajouter RECAPTCHA_SECRET_KEY dans .env
|
||||
- [] Créer includes/config.php pour charger .env et définir les constantes
|
||||
|
||||
- [] **Task 2 : Charger le script Google** (AC: 2)
|
||||
- [] Ajouter le script dans templates/footer.php
|
||||
- [] Charger de manière asynchrone (async defer)
|
||||
- [] Exposer la site key via window.RECAPTCHA_SITE_KEY
|
||||
|
||||
- [] **Task 3 : Générer le token** (AC: 3)
|
||||
- [] Créer RecaptchaService dans contact-form.js
|
||||
- [] Méthode getToken() avec grecaptcha.execute()
|
||||
- [] Retourne une Promise avec le token
|
||||
|
||||
- [] **Task 4 : Envoyer le token au backend** (AC: 4)
|
||||
- [] RecaptchaService.getToken() prêt à être utilisé
|
||||
- [] Intégration avec AJAX dans Story 5.5/5.6
|
||||
|
||||
- [] **Task 5 : Dégradation gracieuse** (AC: 6)
|
||||
- [] isAvailable() vérifie si grecaptcha est défini
|
||||
- [] Retourne chaîne vide si indisponible
|
||||
- [] console.warn si non disponible
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Configuration .env
|
||||
|
||||
```env
|
||||
# reCAPTCHA v3
|
||||
RECAPTCHA_SITE_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
|
||||
RECAPTCHA_SECRET_KEY=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
|
||||
```
|
||||
|
||||
Note : Les clés ci-dessus sont les clés de test de Google (fonctionnent partout mais retournent toujours un score de 0.9).
|
||||
|
||||
### Exposer la Site Key (config.php ou header.php)
|
||||
|
||||
```php
|
||||
<!-- Dans templates/footer.php ou avant </body> -->
|
||||
|
||||
<?php if (defined('RECAPTCHA_SITE_KEY') && RECAPTCHA_SITE_KEY): ?>
|
||||
<script>
|
||||
window.RECAPTCHA_SITE_KEY = '<?= RECAPTCHA_SITE_KEY ?>';
|
||||
</script>
|
||||
<script src="https://www.google.com/recaptcha/api.js?render=<?= RECAPTCHA_SITE_KEY ?>" async defer></script>
|
||||
<?php endif; ?>
|
||||
```
|
||||
|
||||
### Service JavaScript
|
||||
|
||||
```javascript
|
||||
// assets/js/contact-form.js (ajouter)
|
||||
|
||||
/**
|
||||
* Service reCAPTCHA v3
|
||||
*/
|
||||
const RecaptchaService = {
|
||||
siteKey: null,
|
||||
|
||||
init() {
|
||||
this.siteKey = window.RECAPTCHA_SITE_KEY || null;
|
||||
},
|
||||
|
||||
isAvailable() {
|
||||
return this.siteKey && typeof grecaptcha !== 'undefined';
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtient un token reCAPTCHA
|
||||
* @param {string} action - Action à valider (ex: 'contact')
|
||||
* @returns {Promise<string>} - Token ou chaîne vide si indisponible
|
||||
*/
|
||||
async getToken(action = 'contact') {
|
||||
// Dégradation gracieuse si reCAPTCHA non disponible
|
||||
if (!this.isAvailable()) {
|
||||
console.warn('reCAPTCHA non disponible, envoi sans protection');
|
||||
return '';
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
grecaptcha.ready(() => {
|
||||
grecaptcha.execute(this.siteKey, { action })
|
||||
.then(token => resolve(token))
|
||||
.catch(error => {
|
||||
console.error('Erreur reCAPTCHA:', error);
|
||||
resolve(''); // Permettre l'envoi quand même
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Initialiser au chargement
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
RecaptchaService.init();
|
||||
});
|
||||
```
|
||||
|
||||
### Intégration dans l'Envoi du Formulaire
|
||||
|
||||
```javascript
|
||||
// Dans contact-form.js
|
||||
|
||||
async function submitForm(formData) {
|
||||
// Obtenir le token reCAPTCHA
|
||||
const recaptchaToken = await RecaptchaService.getToken('contact');
|
||||
|
||||
// Ajouter aux données
|
||||
const payload = {
|
||||
...formData,
|
||||
recaptcha_token: recaptchaToken
|
||||
};
|
||||
|
||||
// Envoyer au backend
|
||||
const response = await fetch('/api/contact.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
```
|
||||
|
||||
### Vérification Côté Serveur (api/contact.php)
|
||||
|
||||
```php
|
||||
/**
|
||||
* Vérifie le token reCAPTCHA v3 auprès de Google
|
||||
* @param string $token Token reçu du client
|
||||
* @return float Score (0.0 à 1.0), 0.0 si échec
|
||||
*/
|
||||
function verifyRecaptcha(string $token): float
|
||||
{
|
||||
// Si pas de token, retourner un score bas mais pas bloquant
|
||||
if (empty($token)) {
|
||||
error_log('reCAPTCHA: token vide');
|
||||
return 0.3; // Score bas mais pas 0
|
||||
}
|
||||
|
||||
$response = file_get_contents('https://www.google.com/recaptcha/api/siteverify', false, stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => 'Content-Type: application/x-www-form-urlencoded',
|
||||
'content' => http_build_query([
|
||||
'secret' => RECAPTCHA_SECRET_KEY,
|
||||
'response' => $token,
|
||||
'remoteip' => $_SERVER['REMOTE_ADDR'] ?? ''
|
||||
])
|
||||
]
|
||||
]));
|
||||
|
||||
if ($response === false) {
|
||||
error_log('reCAPTCHA: impossible de contacter Google');
|
||||
return 0.3; // Dégradation gracieuse
|
||||
}
|
||||
|
||||
$result = json_decode($response, true);
|
||||
|
||||
if (!($result['success'] ?? false)) {
|
||||
error_log('reCAPTCHA: échec - ' . json_encode($result['error-codes'] ?? []));
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return (float) ($result['score'] ?? 0.0);
|
||||
}
|
||||
```
|
||||
|
||||
### Seuil de Score
|
||||
|
||||
| Score | Interprétation | Action |
|
||||
|-------|----------------|--------|
|
||||
| 0.9 - 1.0 | Très probablement humain | Accepter |
|
||||
| 0.5 - 0.9 | Probablement humain | Accepter |
|
||||
| 0.3 - 0.5 | Douteux | Accepter avec vigilance |
|
||||
| 0.0 - 0.3 | Probablement bot | Rejeter |
|
||||
|
||||
Dans notre implémentation : seuil à 0.5
|
||||
|
||||
### Dégradation Gracieuse
|
||||
|
||||
Si reCAPTCHA échoue :
|
||||
1. Le formulaire reste fonctionnel
|
||||
2. Un avertissement est loggé
|
||||
3. Le backend peut décider d'accepter ou non
|
||||
4. Pas de message d'erreur visible pour l'utilisateur
|
||||
|
||||
## Testing
|
||||
|
||||
- [] Le script reCAPTCHA se charge (vérifier Network)
|
||||
- [] Un token est généré à la soumission (RecaptchaService.getToken)
|
||||
- [] Le token est prêt à être envoyé au backend
|
||||
- [ ] Le backend vérifie le token avec Google (Story 5.5)
|
||||
- [] Si reCAPTCHA indisponible, le formulaire fonctionne quand même
|
||||
- [] Pas d'erreur visible si reCAPTCHA échoue (console.warn seulement)
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### File List
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `includes/config.php` | Created | Chargement .env et définition des constantes |
|
||||
| `.env` | Created | Variables d'environnement (clés test Google) |
|
||||
| `index.php` | Modified | Ajout require config.php |
|
||||
| `templates/footer.php` | Modified | Script reCAPTCHA + window.RECAPTCHA_SITE_KEY |
|
||||
| `assets/js/contact-form.js` | Modified | Ajout RecaptchaService |
|
||||
|
||||
### Completion Notes
|
||||
- Système de chargement .env avec loadEnv() dans config.php
|
||||
- Constantes PHP : RECAPTCHA_SITE_KEY, RECAPTCHA_SECRET_KEY, APP_ENV, etc.
|
||||
- Script Google chargé en async/defer dans footer.php
|
||||
- RecaptchaService avec méthodes init(), isAvailable(), getToken()
|
||||
- Dégradation gracieuse : retourne '' si reCAPTCHA indisponible
|
||||
- Clés de test Google utilisées en développement (score toujours 0.9)
|
||||
- La vérification côté serveur sera implémentée dans Story 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-24 | 1.0 | Implémentation complète | James (Dev) |
|
||||
320
docs/stories/5.5.traitement-php-email.md
Normal file
320
docs/stories/5.5.traitement-php-email.md
Normal file
@@ -0,0 +1,320 @@
|
||||
# Story 5.5: Traitement PHP et Envoi d'Email
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** propriétaire du site,
|
||||
**I want** recevoir les messages par email de manière sécurisée,
|
||||
**so that** je puisse répondre aux visiteurs.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Le backend PHP valide à nouveau tous les champs (ne jamais faire confiance au client)
|
||||
2. Le token reCAPTCHA est vérifié via l'API Google (score > 0.5)
|
||||
3. Les données sont nettoyées (htmlspecialchars, trim) contre XSS
|
||||
4. Un token CSRF est vérifié pour éviter les attaques cross-site
|
||||
5. L'email est envoyé via `mail()` PHP ou SMTP configuré
|
||||
6. L'email contient : tous les champs du formulaire, catégorie, date/heure, IP (optionnel)
|
||||
7. En cas de succès, une réponse JSON `{"success": true}` est renvoyée
|
||||
8. En cas d'erreur, une réponse JSON avec le message d'erreur est renvoyée
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Créer l'endpoint api/contact.php** (AC: 7, 8)
|
||||
- [] Créer le fichier api/contact.php
|
||||
- [] Configurer les headers JSON (Content-Type, X-Content-Type-Options)
|
||||
- [] Gérer uniquement les requêtes POST (405 sinon)
|
||||
|
||||
- [] **Task 2 : Valider le token CSRF** (AC: 4)
|
||||
- [] Récupérer le token de la requête JSON
|
||||
- [] Utiliser verifyCsrfToken() existante
|
||||
- [] Exception si invalide
|
||||
|
||||
- [] **Task 3 : Vérifier reCAPTCHA** (AC: 2)
|
||||
- [] Créer verifyRecaptcha() dans functions.php
|
||||
- [] Appeler l'API Google siteverify
|
||||
- [] Rejeter si score < RECAPTCHA_THRESHOLD (0.5)
|
||||
|
||||
- [] **Task 4 : Valider les données** (AC: 1, 3)
|
||||
- [] Créer validateContactData() dans functions.php
|
||||
- [] Valider required, format email, longueurs min/max
|
||||
- [] Nettoyer avec htmlspecialchars et trim
|
||||
- [] Exception avec messages détaillés
|
||||
|
||||
- [] **Task 5 : Envoyer l'email** (AC: 5, 6)
|
||||
- [] Créer sendContactEmail() dans functions.php
|
||||
- [] Corps formaté avec tous les champs + IP + date
|
||||
- [] Headers avec Reply-To vers l'expéditeur
|
||||
|
||||
- [] **Task 6 : Retourner la réponse** (AC: 7, 8)
|
||||
- [] JSON {"success": true, "message": "..."} si OK
|
||||
- [] JSON {"success": false, "error": "..."} si erreur
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Endpoint api/contact.php
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Endpoint de traitement du formulaire de contact
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../includes/functions.php';
|
||||
|
||||
// Headers
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
|
||||
// Uniquement POST
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'error' => 'Méthode non autorisée']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Récupérer les données JSON
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$input) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Données invalides']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Valider le token CSRF
|
||||
if (!validateCsrfToken($input['csrf_token'] ?? '')) {
|
||||
throw new Exception('Token de sécurité invalide. Veuillez rafraîchir la page.');
|
||||
}
|
||||
|
||||
// 2. Vérifier reCAPTCHA
|
||||
$recaptchaScore = verifyRecaptcha($input['recaptcha_token'] ?? '');
|
||||
if ($recaptchaScore < RECAPTCHA_THRESHOLD) {
|
||||
error_log("reCAPTCHA score trop bas: {$recaptchaScore}");
|
||||
throw new Exception('Vérification anti-spam échouée. Veuillez réessayer.');
|
||||
}
|
||||
|
||||
// 3. Valider et nettoyer les données
|
||||
$data = validateContactData($input);
|
||||
|
||||
// 4. Envoyer l'email
|
||||
$sent = sendContactEmail($data);
|
||||
|
||||
if (!$sent) {
|
||||
throw new Exception('Erreur lors de l\'envoi du message. Veuillez réessayer plus tard.');
|
||||
}
|
||||
|
||||
// 5. Succès
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Votre message a bien été envoyé ! Je vous répondrai dans les meilleurs délais.'
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
### Fonction de Validation (includes/functions.php)
|
||||
|
||||
```php
|
||||
/**
|
||||
* Valide et nettoie les données du formulaire de contact
|
||||
* @throws Exception si validation échoue
|
||||
*/
|
||||
function validateContactData(array $input): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// Champs requis
|
||||
$required = ['nom', 'prenom', 'email', 'categorie', 'objet', 'message'];
|
||||
foreach ($required as $field) {
|
||||
if (empty(trim($input[$field] ?? ''))) {
|
||||
$errors[] = "Le champ {$field} est requis";
|
||||
}
|
||||
}
|
||||
|
||||
// Validation email
|
||||
$email = trim($input['email'] ?? '');
|
||||
if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$errors[] = "L'adresse email n'est pas valide";
|
||||
}
|
||||
|
||||
// Validation catégorie
|
||||
$validCategories = ['projet', 'poste', 'autre'];
|
||||
$categorie = $input['categorie'] ?? '';
|
||||
if ($categorie && !in_array($categorie, $validCategories)) {
|
||||
$errors[] = "Catégorie invalide";
|
||||
}
|
||||
|
||||
// Validation longueurs
|
||||
if (strlen($input['nom'] ?? '') > 100) {
|
||||
$errors[] = "Le nom est trop long (max 100 caractères)";
|
||||
}
|
||||
if (strlen($input['prenom'] ?? '') > 100) {
|
||||
$errors[] = "Le prénom est trop long (max 100 caractères)";
|
||||
}
|
||||
if (strlen($input['objet'] ?? '') > 200) {
|
||||
$errors[] = "L'objet est trop long (max 200 caractères)";
|
||||
}
|
||||
if (strlen($input['message'] ?? '') > 5000) {
|
||||
$errors[] = "Le message est trop long (max 5000 caractères)";
|
||||
}
|
||||
|
||||
// Si erreurs, les lancer
|
||||
if (!empty($errors)) {
|
||||
throw new Exception(implode('. ', $errors));
|
||||
}
|
||||
|
||||
// Nettoyer et retourner
|
||||
return [
|
||||
'nom' => htmlspecialchars(trim($input['nom']), ENT_QUOTES, 'UTF-8'),
|
||||
'prenom' => htmlspecialchars(trim($input['prenom']), ENT_QUOTES, 'UTF-8'),
|
||||
'email' => filter_var(trim($input['email']), FILTER_SANITIZE_EMAIL),
|
||||
'entreprise' => htmlspecialchars(trim($input['entreprise'] ?? ''), ENT_QUOTES, 'UTF-8'),
|
||||
'categorie' => $input['categorie'],
|
||||
'objet' => htmlspecialchars(trim($input['objet']), ENT_QUOTES, 'UTF-8'),
|
||||
'message' => htmlspecialchars(trim($input['message']), ENT_QUOTES, 'UTF-8'),
|
||||
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'inconnue',
|
||||
'date' => date('d/m/Y à H:i:s'),
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Fonction d'Envoi d'Email
|
||||
|
||||
```php
|
||||
/**
|
||||
* Envoie l'email de contact
|
||||
*/
|
||||
function sendContactEmail(array $data): bool
|
||||
{
|
||||
$categorieLabels = [
|
||||
'projet' => 'Projet freelance',
|
||||
'poste' => 'Proposition de poste',
|
||||
'autre' => 'Autre demande'
|
||||
];
|
||||
|
||||
$subject = "[Portfolio] {$categorieLabels[$data['categorie']]} - {$data['objet']}";
|
||||
|
||||
$body = <<<EMAIL
|
||||
═══════════════════════════════════════════
|
||||
NOUVEAU MESSAGE - PORTFOLIO
|
||||
═══════════════════════════════════════════
|
||||
|
||||
DE: {$data['prenom']} {$data['nom']}
|
||||
EMAIL: {$data['email']}
|
||||
ENTREPRISE: {$data['entreprise']}
|
||||
CATÉGORIE: {$categorieLabels[$data['categorie']]}
|
||||
|
||||
───────────────────────────────────────────
|
||||
OBJET: {$data['objet']}
|
||||
───────────────────────────────────────────
|
||||
|
||||
MESSAGE:
|
||||
|
||||
{$data['message']}
|
||||
|
||||
═══════════════════════════════════════════
|
||||
Envoyé le {$data['date']}
|
||||
IP: {$data['ip']}
|
||||
═══════════════════════════════════════════
|
||||
EMAIL;
|
||||
|
||||
$headers = implode("\r\n", [
|
||||
'From: ' . CONTACT_EMAIL,
|
||||
'Reply-To: ' . $data['email'],
|
||||
'Content-Type: text/plain; charset=UTF-8',
|
||||
'X-Mailer: PHP/' . phpversion(),
|
||||
'X-Priority: 1'
|
||||
]);
|
||||
|
||||
$result = mail(CONTACT_EMAIL, $subject, $body, $headers);
|
||||
|
||||
if (!$result) {
|
||||
error_log("Échec envoi email contact: " . print_r($data, true));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
```
|
||||
|
||||
### Sécurité Implémentée
|
||||
|
||||
| Menace | Protection |
|
||||
|--------|------------|
|
||||
| XSS | htmlspecialchars() sur toutes les entrées |
|
||||
| CSRF | Token vérifié en session |
|
||||
| Spam | reCAPTCHA v3 avec seuil 0.5 |
|
||||
| Injection | filter_var() pour l'email |
|
||||
| Email header injection | Pas de \r\n dans les champs utilisateur |
|
||||
|
||||
### Structure de la Réponse JSON
|
||||
|
||||
**Succès :**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Votre message a bien été envoyé !"
|
||||
}
|
||||
```
|
||||
|
||||
**Erreur :**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Message d'erreur explicite"
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
- [] Les données sont validées côté serveur (validateContactData)
|
||||
- [] Le token CSRF est vérifié (verifyCsrfToken)
|
||||
- [] Le score reCAPTCHA est vérifié (verifyRecaptcha)
|
||||
- [] Les données sont nettoyées (htmlspecialchars, filter_var)
|
||||
- [] L'email est envoyé avec tous les champs (sendContactEmail)
|
||||
- [] La réponse JSON est correcte (succès)
|
||||
- [] La réponse JSON est correcte (erreur avec message)
|
||||
- [] Les erreurs sont loggées (error_log)
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### File List
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `api/contact.php` | Created | Endpoint de traitement du formulaire |
|
||||
| `includes/functions.php` | Modified | Ajout verifyRecaptcha, validateContactData, sendContactEmail |
|
||||
| `includes/config.php` | Modified | Ajout RECAPTCHA_THRESHOLD |
|
||||
|
||||
### Completion Notes
|
||||
- Endpoint api/contact.php avec gestion JSON complète
|
||||
- verifyRecaptcha() : appel API Google avec dégradation gracieuse (0.3 si échec)
|
||||
- validateContactData() : validation complète (required, email, longueurs min/max, catégorie)
|
||||
- sendContactEmail() : email formaté avec tous les champs, Reply-To, IP, date
|
||||
- Sécurité : CSRF, reCAPTCHA, htmlspecialchars, filter_var
|
||||
- Réponses JSON standardisées {success, message/error}
|
||||
- Logging des erreurs via error_log()
|
||||
|
||||
### Debug Log References
|
||||
- Correction syntaxe heredoc (EMAIL: interprété comme label)
|
||||
|
||||
## 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) |
|
||||
294
docs/stories/5.6.feedback-utilisateur.md
Normal file
294
docs/stories/5.6.feedback-utilisateur.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# Story 5.6: Feedback Utilisateur (Succès/Erreur)
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## 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
|
||||
|
||||
- [] **Task 1 : Afficher l'état de chargement** (AC: 1, 2)
|
||||
- [] Masquer le texte du bouton (submitText.classList.add('hidden'))
|
||||
- [] Afficher le spinner (submitLoading.classList.remove('hidden'))
|
||||
- [] Désactiver le bouton (submitBtn.disabled = true)
|
||||
|
||||
- [] **Task 2 : Envoyer en AJAX** (AC: 5)
|
||||
- [] Utiliser fetch() avec POST
|
||||
- [] Envoyer les données en JSON (Content-Type: application/json)
|
||||
- [] Inclure les tokens (CSRF + reCAPTCHA)
|
||||
|
||||
- [] **Task 3 : Gérer le succès** (AC: 3, 6)
|
||||
- [] Masquer le formulaire (form.classList.add('hidden'))
|
||||
- [] Afficher le message de succès
|
||||
- [] Mention des spams (vérifier sous 48h)
|
||||
- [] Vider le localStorage (AppState.clearFormData())
|
||||
- [] Réinitialiser le formulaire (form.reset())
|
||||
|
||||
- [] **Task 4 : Gérer les erreurs** (AC: 4)
|
||||
- [] Afficher le message d'erreur avec icône
|
||||
- [] Garder les données dans le formulaire
|
||||
- [] Message "Vos données ont été conservées"
|
||||
- [] Permettre de réessayer
|
||||
|
||||
- [] **Task 5 : Réinitialiser l'état après feedback**
|
||||
- [] Masquer le spinner (finally block)
|
||||
- [] Réactiver le bouton
|
||||
- [] 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
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### File List
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `assets/js/contact-form.js` | Modified | Ajout classe ContactFormSubmit |
|
||||
| `pages/contact.php` | Modified | Messages succès/erreur améliorés |
|
||||
|
||||
### Completion Notes
|
||||
- Classe ContactFormSubmit avec gestion complète du cycle de vie
|
||||
- État loading : spinner + bouton désactivé
|
||||
- Envoi AJAX avec fetch() et JSON
|
||||
- Tokens CSRF et reCAPTCHA inclus automatiquement
|
||||
- Succès : formulaire masqué, message avec mention spams, localStorage vidé
|
||||
- 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
|
||||
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) |
|
||||
209
docs/stories/5.7.liens-contact-secondaires.md
Normal file
209
docs/stories/5.7.liens-contact-secondaires.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# Story 5.7: Liens de Contact Secondaires
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** visiteur,
|
||||
**I want** avoir des alternatives au formulaire pour contacter le développeur,
|
||||
**so that** je choisis le canal qui me convient.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Sous le formulaire, une section affiche les liens secondaires : LinkedIn, GitHub, email direct (mailto)
|
||||
2. Les liens sont affichés avec leurs icônes respectives
|
||||
3. Les liens s'ouvrent dans un nouvel onglet (sauf mailto)
|
||||
4. La section est visuellement distincte mais cohérente avec le formulaire
|
||||
5. L'adresse email est protégée contre le scraping (encodage ou JS)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Ajouter la section dans contact.php** (AC: 1, 4)
|
||||
- [] Titre "Retrouvez-moi aussi sur"
|
||||
- [] Positionnement sous le formulaire (mt-16, pt-8, border-t)
|
||||
- [] Style distinct mais cohérent (bg-surface-alt, border)
|
||||
|
||||
- [] **Task 2 : Ajouter les liens avec icônes** (AC: 2)
|
||||
- [] LinkedIn avec icône SVG (#0A66C2)
|
||||
- [] GitHub avec icône SVG
|
||||
- [] Email avec icône SVG (primary)
|
||||
|
||||
- [] **Task 3 : Configurer les liens** (AC: 3)
|
||||
- [] `target="_blank"` + `rel="noopener noreferrer"` pour LinkedIn/GitHub
|
||||
- [] `mailto:` généré par JS pour l'email
|
||||
|
||||
- [] **Task 4 : Protéger l'email** (AC: 5)
|
||||
- [] data-user et data-domain dans le HTML
|
||||
- [] initEmailProtection() dans main.js
|
||||
- [] Reconstruction du mailto au chargement
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Section à ajouter dans contact.php
|
||||
|
||||
```php
|
||||
<!-- Sous le formulaire et les messages de feedback -->
|
||||
|
||||
<!-- Liens secondaires -->
|
||||
<section class="mt-16 pt-8 border-t border-border">
|
||||
<h2 class="text-lg font-semibold text-center mb-6">Retrouvez-moi aussi sur</h2>
|
||||
|
||||
<div class="flex flex-wrap justify-center gap-6">
|
||||
<!-- LinkedIn -->
|
||||
<a
|
||||
href="https://linkedin.com/in/votre-profil"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="flex items-center gap-3 px-6 py-3 bg-surface rounded-lg hover:bg-surface-light transition-colors group"
|
||||
aria-label="Profil LinkedIn"
|
||||
>
|
||||
<svg class="w-6 h-6 text-[#0A66C2]" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
||||
</svg>
|
||||
<span class="font-medium text-text-primary group-hover:text-primary transition-colors">LinkedIn</span>
|
||||
</a>
|
||||
|
||||
<!-- GitHub -->
|
||||
<a
|
||||
href="https://github.com/votre-username"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="flex items-center gap-3 px-6 py-3 bg-surface rounded-lg hover:bg-surface-light transition-colors group"
|
||||
aria-label="Profil GitHub"
|
||||
>
|
||||
<svg class="w-6 h-6 text-text-primary" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
<span class="font-medium text-text-primary group-hover:text-primary transition-colors">GitHub</span>
|
||||
</a>
|
||||
|
||||
<!-- Email (protégé) -->
|
||||
<a
|
||||
href="#"
|
||||
id="email-link"
|
||||
class="flex items-center gap-3 px-6 py-3 bg-surface rounded-lg hover:bg-surface-light transition-colors group"
|
||||
aria-label="Envoyer un email"
|
||||
data-user="contact"
|
||||
data-domain="monportfolio.fr"
|
||||
>
|
||||
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<span class="font-medium text-text-primary group-hover:text-primary transition-colors">Email</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
### Protection de l'Email (JavaScript)
|
||||
|
||||
```javascript
|
||||
// assets/js/main.js (ajouter)
|
||||
|
||||
/**
|
||||
* Protection de l'email contre le scraping
|
||||
* Reconstruit l'adresse email à partir de data-attributes
|
||||
*/
|
||||
function initEmailProtection() {
|
||||
const emailLink = document.getElementById('email-link');
|
||||
if (!emailLink) return;
|
||||
|
||||
const user = emailLink.dataset.user;
|
||||
const domain = emailLink.dataset.domain;
|
||||
|
||||
if (user && domain) {
|
||||
const email = `${user}@${domain}`;
|
||||
emailLink.href = `mailto:${email}`;
|
||||
|
||||
// Optionnel : afficher l'email au hover
|
||||
emailLink.title = email;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initEmailProtection();
|
||||
});
|
||||
```
|
||||
|
||||
### Alternative : Encodage HTML
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Encode une adresse email en entités HTML
|
||||
*/
|
||||
function encodeEmail(string $email): string
|
||||
{
|
||||
$encoded = '';
|
||||
for ($i = 0; $i < strlen($email); $i++) {
|
||||
$encoded .= '&#' . ord($email[$i]) . ';';
|
||||
}
|
||||
return $encoded;
|
||||
}
|
||||
|
||||
$email = 'contact@monportfolio.fr';
|
||||
$encodedEmail = encodeEmail($email);
|
||||
?>
|
||||
|
||||
<a href="mailto:<?= $encodedEmail ?>">
|
||||
<?= $encodedEmail ?>
|
||||
</a>
|
||||
```
|
||||
|
||||
### Liens à Configurer
|
||||
|
||||
| Plateforme | URL | Notes |
|
||||
|------------|-----|-------|
|
||||
| LinkedIn | https://linkedin.com/in/votre-profil | Profil public |
|
||||
| GitHub | https://github.com/votre-username | Profil public |
|
||||
| Email | contact@monportfolio.fr | Via .env ou config |
|
||||
|
||||
### Couleurs des Icônes
|
||||
|
||||
| Plateforme | Couleur | Tailwind |
|
||||
|------------|---------|----------|
|
||||
| LinkedIn | #0A66C2 | `text-[#0A66C2]` |
|
||||
| GitHub | Inherit (blanc) | `text-text-primary` |
|
||||
| Email | Primary | `text-primary` |
|
||||
|
||||
## Testing
|
||||
|
||||
- [] Les 3 liens sont affichés avec leurs icônes
|
||||
- [] LinkedIn et GitHub s'ouvrent dans un nouvel onglet
|
||||
- [] Le lien mailto déclenche le client email
|
||||
- [] L'email n'apparaît pas en clair dans le HTML source (data-attributes)
|
||||
- [] Le JS reconstruit correctement le lien mailto
|
||||
- [] Les hover states fonctionnent (border-primary/50)
|
||||
- [] La section est visuellement distincte (border-t, mt-16)
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### File List
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `pages/contact.php` | Modified | Section liens secondaires (LinkedIn, GitHub, Email) |
|
||||
| `assets/js/main.js` | Modified | Ajout initEmailProtection() |
|
||||
|
||||
### Completion Notes
|
||||
- Section "Retrouvez-moi aussi sur" avec 3 liens
|
||||
- LinkedIn : https://linkedin.com/in/celian-music (à personnaliser)
|
||||
- GitHub : https://github.com/skycel
|
||||
- Email protégé : data-user/data-domain + reconstruction JS
|
||||
- Icônes SVG avec couleurs appropriées (LinkedIn bleu, GitHub inherit, Email primary)
|
||||
- Hover state : border-primary/50
|
||||
- target="_blank" + rel="noopener noreferrer" pour les liens externes
|
||||
|
||||
### 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) |
|
||||
67
docs/stories/6.1.correctif-contact-form-production.md
Normal file
67
docs/stories/6.1.correctif-contact-form-production.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Story 6.1 - Correctif contact form production
|
||||
|
||||
## Status
|
||||
|
||||
Ready for Dev
|
||||
|
||||
## Story
|
||||
|
||||
**As a** developer
|
||||
**I want** corriger le formulaire de contact non fonctionnel en production
|
||||
**So that** le site fonctionne correctement
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Le formulaire de contact fonctionne correctement en production
|
||||
2. Usage de PHPMailer pour ne pas dépendre d'une installation lourde sur le serveur
|
||||
3. Ajout des variables d'environnement dans le fichier .env
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [] **Task 1 : Ajout de PHPMailer**
|
||||
- [] Installation de PHPMailer
|
||||
- [] Utilisation de PHPMailer pour envoyer un mail
|
||||
|
||||
- [] **Task 2 : Ajout des variables d'environnement pour PHPMailer**
|
||||
- [] Ajout des variables d'environnement dans le fichier .env
|
||||
- [] Ajout des variables d'environnement dans le fichier .env.example
|
||||
- [] Configuration des constantes basées sur les variables d'environnement
|
||||
|
||||
- [] **Task 3 : Intégrer PHPMailer dans le formulaire de contact**
|
||||
- [] Modification de la fonction sendContactMail() pour utiliser PHPMailer
|
||||
- [] Modification de l'endpoint /api/contact pour utiliser PHPMailer
|
||||
- [] Test de l'envoi d'un mail avec PHPMailer
|
||||
|
||||
- [] **Task 4 : Tester le formulaire de contact en production**
|
||||
- [] Tester le formulaire de contact en production
|
||||
|
||||
## Dev Notes
|
||||
|
||||
## Testing
|
||||
|
||||
- [] Tester l'envoi d'un mail avec PHPMailer
|
||||
- [] Tester le formulaire de contact en local
|
||||
- [] Tester le formulaire de contact en production
|
||||
- [] Vérifier la réception du mail
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
### File list
|
||||
| File | Action | Description |
|
||||
|--------------------------|--------|-------------|
|
||||
| `includes/functions.php` | Modified | Modification de la fonction sendContactMail() pour utiliser PHPMailer |
|
||||
| `api/contact.php` | Modified | Modification de l'endpoint /api/contact pour utiliser PHPMailer |
|
||||
|
||||
### Completion Notes
|
||||
- Utilisation de PHPMailer pour envoyer un mail
|
||||
|
||||
### Debug Log References
|
||||
|
||||
## Change Log
|
||||
|
||||
| Date | Version | Description | Author |
|
||||
|------------|---------|-------------------------|---------------------|
|
||||
| 2026-01-24 | 0.1 | Creation du story | Skycel (developper) |
|
||||
| 2026-01-24 | 1.0 | Implémentation complète | Skycel (developper) |
|
||||
1564
docs/ui-architecture.md
Normal file
1564
docs/ui-architecture.md
Normal file
File diff suppressed because it is too large
Load Diff
11
index.php
Normal file
11
index.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hello World</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
</body>
|
||||
</html>
|
||||
0
logs/.gitkeep
Normal file
0
logs/.gitkeep
Normal file
4
tests/run.ps1
Normal file
4
tests/run.ps1
Normal file
@@ -0,0 +1,4 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
& (Join-Path $here 'structure.test.ps1')
|
||||
'OK'
|
||||
73
tests/structure.test.ps1
Normal file
73
tests/structure.test.ps1
Normal file
@@ -0,0 +1,73 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Assert-True {
|
||||
param(
|
||||
[bool]$Condition,
|
||||
[string]$Message
|
||||
)
|
||||
if (-not $Condition) { throw $Message }
|
||||
}
|
||||
|
||||
$dirs = @(
|
||||
'pages',
|
||||
'templates',
|
||||
'includes',
|
||||
'api',
|
||||
'assets/css',
|
||||
'assets/js',
|
||||
'assets/img',
|
||||
'assets/img/projects',
|
||||
'assets/fonts',
|
||||
'data',
|
||||
'logs'
|
||||
)
|
||||
|
||||
foreach ($d in $dirs) {
|
||||
Assert-True (Test-Path $d) "Missing dir: $d"
|
||||
}
|
||||
|
||||
Assert-True (Test-Path 'logs/.gitkeep') 'Missing logs/.gitkeep'
|
||||
|
||||
Assert-True (Test-Path 'index.php') 'Missing index.php'
|
||||
$index = Get-Content -Raw 'index.php'
|
||||
Assert-True ($index -match 'Hello World') 'index.php missing Hello World'
|
||||
Assert-True ($index -match 'meta name="viewport"') 'index.php missing viewport meta'
|
||||
|
||||
Assert-True (Test-Path '.gitignore') 'Missing .gitignore'
|
||||
$gitignore = Get-Content -Raw '.gitignore'
|
||||
$required = @(
|
||||
'.env',
|
||||
'vendor/',
|
||||
'node_modules/',
|
||||
'logs/*.log',
|
||||
'assets/css/output.css',
|
||||
'.idea/',
|
||||
'.vscode/',
|
||||
'.DS_Store'
|
||||
)
|
||||
foreach ($r in $required) {
|
||||
Assert-True ($gitignore -match [regex]::Escape($r)) "Missing .gitignore entry: $r"
|
||||
}
|
||||
|
||||
Assert-True (Test-Path '.env.example') 'Missing .env.example'
|
||||
$env = Get-Content -Raw '.env.example'
|
||||
$requiredEnv = @(
|
||||
'APP_ENV=',
|
||||
'APP_DEBUG=',
|
||||
'APP_URL=',
|
||||
'RECAPTCHA_SITE_KEY=',
|
||||
'RECAPTCHA_SECRET_KEY=',
|
||||
'CONTACT_EMAIL=',
|
||||
'APP_SECRET='
|
||||
)
|
||||
foreach ($r in $requiredEnv) {
|
||||
Assert-True ($env -match [regex]::Escape($r)) "Missing env var: $r"
|
||||
}
|
||||
|
||||
Assert-True (Test-Path 'composer.json') 'Missing composer.json'
|
||||
$composer = Get-Content -Raw 'composer.json' | ConvertFrom-Json
|
||||
$dotenv = $composer.require.'vlucas/phpdotenv'
|
||||
$phpReq = $composer.require.php
|
||||
Assert-True (-not [string]::IsNullOrWhiteSpace($dotenv)) 'Missing vlucas/phpdotenv dependency'
|
||||
Assert-True (-not [string]::IsNullOrWhiteSpace($phpReq)) 'Missing php requirement'
|
||||
'OK'
|
||||
Reference in New Issue
Block a user