Compare commits

..

2 Commits

Author SHA1 Message Date
d3e699d00e Story 3.5: projets secondaires 2026-02-04 17:14:37 +01:00
475a8f5457 Story 3.4: page projet individuelle 2026-02-04 17:11:44 +01:00
8 changed files with 319 additions and 53 deletions

View File

@@ -2,7 +2,7 @@
## Status
Ready for Dev
review
## Story
@@ -23,38 +23,38 @@ Ready for Dev
## 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é
- [x] **Task 1 : Créer la page project-single.php** (AC: 1, 2, 8)
- [x] Créer `pages/project-single.php`
- [x] Récupérer le slug depuis `$GLOBALS['routeParams']`
- [x] Charger le projet avec `getProjectBySlug()`
- [x] 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
- [x] **Task 2 : Afficher les informations principales** (AC: 3, 5)
- [x] Titre du projet
- [x] Badges technologies
- [x] Section Contexte
- [x] Section Solution technique
- [x] Section Travail d'équipe (si non null)
- [x] 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
- [x] **Task 3 : Ajouter le lien vers le projet** (AC: 4)
- [x] Bouton "Voir le projet en ligne" si URL disponible
- [x] Bouton "Voir sur GitHub" si URL GitHub disponible
- [x] 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
- [x] **Task 4 : Afficher la galerie de captures** (AC: 6)
- [x] Grille de screenshots
- [x] 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
- [x] **Task 5 : Ajouter le témoignage** (AC: 3)
- [x] 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
- [x] **Task 6 : Ajouter la navigation** (AC: 7)
- [x] Breadcrumb en haut de page
- [x] Lien "Retour aux projets"
- [x] CTA "Me contacter" en bas
## Dev Notes
@@ -287,12 +287,14 @@ include_template('navbar', compact('currentPage'));
## Dev Agent Record
### Agent Model Used
Claude Opus 4.5 (claude-opus-4-5-20251101)
GPT-5 Codex
### File List
| File | Action | Description |
|------|--------|-------------|
| `pages/project-single.php` | Modified | Page projet individuelle complète |
| `tests/project-single.test.php` | Created | Tests page projet individuelle |
| `tests/run.ps1` | Modified | Ajout tests projet individuel |
### Completion Notes
- Récupération slug via router ($GLOBALS['routeParams'])
@@ -305,6 +307,7 @@ Claude Opus 4.5 (claude-opus-4-5-20251101)
- Navigation: retour + CTA contact
- Lazy loading + fallback onerror sur images
- Témoignage: placeholder préparé pour Story 4.5
- Tests: `powershell -ExecutionPolicy Bypass -File tests/run.ps1`
### Debug Log References
Aucun problème rencontré.
@@ -314,4 +317,4 @@ Aucun problème rencontré.
| Date | Version | Description | Author |
|------|---------|-------------|--------|
| 2026-01-22 | 0.1 | Création initiale | Sarah (PO) |
| 2026-01-23 | 1.0 | Implémentation complète | James (Dev) |
| 2026-02-04 | 1.0 | Implémentation complète | Amelia |

View File

@@ -2,7 +2,7 @@
## Status
Ready for Dev
review
## Story
@@ -20,20 +20,20 @@ Ready for Dev
## 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
- [x] **Task 1 : Ajouter la section dans projects.php** (AC: 1, 5)
- [x] Récupérer les projets secondaires
- [x] Ajouter un titre de section "Autres projets"
- [x] 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)
- [x] **Task 2 : Créer le template project-card-compact.php** (AC: 2, 3)
- [x] Format liste horizontale
- [x] Titre cliquable (si URL)
- [x] Description courte (truncate si nécessaire)
- [x] Badges technologies (3 max)
- [] **Task 3 : Gérer les liens** (AC: 4)
- [] Si URL → lien externe (nouvel onglet)
- [] Si pas d'URL → texte simple
- [x] **Task 3 : Gérer les liens** (AC: 4)
- [x] Si URL → lien externe (nouvel onglet)
- [x] Si pas d'URL → texte simple
## Dev Notes
@@ -143,20 +143,22 @@ $shortContext = strlen($context) > 100
## Dev Agent Record
### Agent Model Used
Claude Opus 4.5 (claude-opus-4-5-20251101)
GPT-5 Codex
### File List
| File | Action | Description |
|------|--------|-------------|
| `pages/projects.php` | Modified | Ajout section projets secondaires |
| `templates/project-card-compact.php` | Created | Template carte compacte |
| `tests/projects-secondary.test.php` | Created | Tests projets secondaires |
| `tests/run.ps1` | Modified | Ajout tests secondaires |
### 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"
- Tests: `powershell -ExecutionPolicy Bypass -File tests/run.ps1`
### Debug Log References
Aucun problème rencontré.
@@ -166,4 +168,4 @@ Aucun problème rencontré.
| Date | Version | Description | Author |
|------|---------|-------------|--------|
| 2026-01-22 | 0.1 | Création initiale | Sarah (PO) |
| 2026-01-23 | 1.0 | Implémentation complète | James (Dev) |
| 2026-02-04 | 1.0 | Implémentation complète | Amelia |

View File

@@ -1,24 +1,170 @@
<?php
$currentPage = '';
$slug = $GLOBALS['routeParams'][0] ?? null;
$project = $slug ? getProjectBySlug($slug) : null;
/**
* Page projet individuelle
*/
$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;
}
$testimonial = function_exists('getTestimonialByProject') ? getTestimonialByProject($slug) : null;
$pageTitle = $project['title'] ?? 'Projet';
include_template('header', compact('pageTitle'));
$pageDescription = $project['context'] ?? "Découvrez le projet {$pageTitle}";
$currentPage = 'projects';
include_template('header', compact('pageTitle', 'pageDescription'));
include_template('navbar', compact('currentPage'));
?>
<main class="min-h-screen">
<div class="container-content py-20">
<h1 class="text-heading mb-4"><?= htmlspecialchars($project['title'], ENT_QUOTES, 'UTF-8') ?></h1>
<p class="text-text-secondary">Page projet en construction.</p>
<main>
<article class="section">
<div class="container-content">
<nav class="breadcrumb mb-8" aria-label="Breadcrumb">
<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'], ENT_QUOTES, 'UTF-8') ?></span>
</nav>
<header class="mb-12">
<h1 class="text-display mb-4"><?= htmlspecialchars($project['title'], ENT_QUOTES, 'UTF-8') ?></h1>
<div class="flex flex-wrap gap-2 mb-6">
<?php foreach ($project['technologies'] ?? [] as $tech): ?>
<span class="badge badge-primary"><?= htmlspecialchars($tech, ENT_QUOTES, 'UTF-8') ?></span>
<?php endforeach; ?>
</div>
<div class="flex flex-wrap gap-4">
<?php if (!empty($project['url'])): ?>
<a href="<?= htmlspecialchars($project['url'], ENT_QUOTES, 'UTF-8') ?>" 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'], ENT_QUOTES, 'UTF-8') ?>" 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>
<?php if (!empty($project['thumbnail'])): ?>
<div class="mb-12 rounded-lg overflow-hidden">
<img
src="/assets/img/projects/<?= htmlspecialchars($project['thumbnail'], ENT_QUOTES, 'UTF-8') ?>"
alt="<?= htmlspecialchars($project['title'], ENT_QUOTES, 'UTF-8') ?>"
class="w-full"
loading="lazy"
onerror="this.onerror=null;this.src='/assets/img/projects/default-project.svg';"
>
</div>
<?php endif; ?>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-12">
<div class="lg:col-span-2 space-y-10">
<?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'], ENT_QUOTES, 'UTF-8')) ?>
</p>
</section>
<?php endif; ?>
<?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'], ENT_QUOTES, 'UTF-8')) ?>
</p>
</section>
<?php endif; ?>
<?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'], ENT_QUOTES, 'UTF-8')) ?>
</p>
</section>
<?php endif; ?>
<?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, ENT_QUOTES, 'UTF-8') ?>"
alt="Capture d'écran - <?= htmlspecialchars($project['title'], ENT_QUOTES, 'UTF-8') ?>"
class="rounded-lg"
loading="lazy"
onerror="this.onerror=null;this.src='/assets/img/projects/default-project.svg';"
>
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
</div>
<aside class="space-y-6">
<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', ENT_QUOTES, 'UTF-8') ?></p>
</div>
</div>
<?php if ($testimonial): ?>
<div class="testimonial">
<blockquote class="text-text-secondary italic mb-4">
"<?= htmlspecialchars($testimonial['quote'], ENT_QUOTES, 'UTF-8') ?>"
</blockquote>
<footer>
<p class="font-medium text-text-primary"><?= htmlspecialchars($testimonial['author_name'], ENT_QUOTES, 'UTF-8') ?></p>
<p class="text-sm text-text-muted">
<?= htmlspecialchars($testimonial['author_role'], ENT_QUOTES, 'UTF-8') ?>
<?php if (!empty($testimonial['author_company'])): ?>
- <?= htmlspecialchars($testimonial['author_company'], ENT_QUOTES, 'UTF-8') ?>
<?php endif; ?>
</p>
</footer>
</div>
<?php endif; ?>
</aside>
</div>
<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">
&larr; Retour aux projets
</a>
<a href="/contact" class="btn-primary">
Me contacter
</a>
</footer>
</div>
</article>
</main>
<?php include_template('footer'); ?>

View File

@@ -4,6 +4,7 @@ $pageDescription = 'Découvrez mes réalisations web : sites vitrines, e-commerc
$currentPage = 'projects';
$featuredProjects = getProjectsByCategory('vedette');
$secondaryProjects = getProjectsByCategory('secondaire');
include_template('header', compact('pageTitle', 'pageDescription'));
include_template('navbar', compact('currentPage'));
@@ -30,6 +31,19 @@ include_template('navbar', compact('currentPage'));
Projets à venir...
</p>
<?php endif; ?>
<?php if (!empty($secondaryProjects)): ?>
<hr class="border-border my-16">
<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; ?>
</div>
</section>
</main>

View File

@@ -0,0 +1,54 @@
<?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;
$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">
<div class="flex-grow">
<?php if ($url): ?>
<a href="<?= htmlspecialchars($url, ENT_QUOTES, 'UTF-8') ?>"
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, ENT_QUOTES, 'UTF-8') ?>
<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, ENT_QUOTES, 'UTF-8') ?>
</h3>
<?php endif; ?>
<?php if ($shortContext): ?>
<p class="text-text-secondary text-sm mt-1">
<?= htmlspecialchars($shortContext, ENT_QUOTES, 'UTF-8') ?>
</p>
<?php endif; ?>
</div>
<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, ENT_QUOTES, 'UTF-8') ?></span>
<?php endforeach; ?>
<?php if (count($technologies) > $maxTechs): ?>
<span class="badge badge-muted text-xs">+<?= count($technologies) - $maxTechs ?></span>
<?php endif; ?>
</div>
</div>
</article>

View File

@@ -0,0 +1,21 @@
<?php
require_once __DIR__ . '/../includes/functions.php';
function assertTrue($cond, $msg) {
if (!$cond) {
fwrite(STDERR, $msg . PHP_EOL);
exit(1);
}
}
$project = getProjectBySlug('ecommerce-xyz');
assertTrue(is_array($project), 'project not found');
assertTrue(getProjectBySlug('inexistant') === null, 'missing project should be null');
$content = file_get_contents(__DIR__ . '/../pages/project-single.php');
assertTrue(strpos($content, 'Contexte') !== false, 'missing contexte section');
assertTrue(strpos($content, 'Solution Technique') !== false, 'missing solution section');
assertTrue(strpos($content, 'Retour aux projets') !== false, 'missing back link');
assertTrue(strpos($content, 'badge badge-primary') !== false, 'missing technology badges');
fwrite(STDOUT, "OK\n");

View File

@@ -0,0 +1,24 @@
<?php
require_once __DIR__ . '/../includes/functions.php';
function assertTrue($cond, $msg) {
if (!$cond) {
fwrite(STDERR, $msg . PHP_EOL);
exit(1);
}
}
$secondary = getProjectsByCategory('secondaire');
assertTrue(count($secondary) >= 1, 'expected secondary projects');
ob_start();
foreach ($secondary as $project) {
include __DIR__ . '/../templates/project-card-compact.php';
}
$html = ob_get_clean();
assertTrue(strpos($html, 'Autres projets') === false, 'template should not include section title');
assertTrue(strpos($html, 'badge') !== false, 'missing tech badges');
assertTrue(strpos($html, 'target="_blank"') !== false, 'missing external link');
fwrite(STDOUT, "OK\n");

View File

@@ -12,4 +12,6 @@ $here = Split-Path -Parent $MyInvocation.MyCommand.Path
php (Join-Path $here 'projects.test.php')
php (Join-Path $here 'router.test.php')
php (Join-Path $here 'projects-list.test.php')
php (Join-Path $here 'project-single.test.php')
php (Join-Path $here 'projects-secondary.test.php')
'OK'