# Story 2.3: Page Projet - Détail Status: review ## Story As a visiteur, I want voir les détails d'un projet spécifique, so that je comprends le travail réalisé et les technologies utilisées. ## Acceptance Criteria 1. **Given** le visiteur accède à `/projets/{slug}` (FR) ou `/en/projects/{slug}` (EN) **When** la page se charge **Then** le titre, la description complète et l'image principale du projet s'affichent 2. **And** la date de réalisation est visible 3. **And** la liste des compétences utilisées s'affiche avec leurs niveaux (avant/après le projet) 4. **And** les liens externes sont présents : URL du projet live (si existe), repository GitHub (si existe) 5. **And** une navigation "Projet précédent / Projet suivant" permet de parcourir les projets 6. **And** un bouton retour vers la galerie est visible 7. **And** les meta tags SEO sont dynamiques (titre, description, image Open Graph) 8. **And** si le slug n'existe pas, une page 404 appropriée s'affiche 9. **And** le design est responsive (adaptation mobile/desktop) ## Tasks / Subtasks - [x] **Task 1: Créer l'endpoint API pour le détail du projet** (AC: #1, #2, #3, #4, #8) - [x] Ajouter la méthode `show($slug)` dans `ProjectController` - [x] Charger le projet avec ses compétences (eager loading) - [x] Retourner 404 si le slug n'existe pas - [x] Inclure les données de traduction selon `Accept-Language` - [x] **Task 2: Créer l'endpoint API pour la navigation prev/next** (AC: #5) - [x] Ajouter une méthode `navigation($slug)` ou inclure dans `show()` - [x] Retourner le projet précédent et suivant (basé sur l'ordre de tri) - [x] Si premier projet : prev = null, si dernier : next = null - [x] **Task 3: Créer le composable useFetchProject** (AC: #1) - [x] Créer `frontend/app/composables/useFetchProject.ts` - [x] Accepter le slug en paramètre - [x] Gérer les états loading, error, data - [x] Gérer l'erreur 404 - [x] **Task 4: Créer la page [slug].vue** (AC: #1, #2, #3, #4, #6, #9) - [x] Créer `frontend/app/pages/projets/[slug].vue` - [x] Afficher l'image principale en grand format - [x] Afficher le titre et la description complète - [x] Afficher la date de réalisation formatée - [x] Afficher la liste des compétences avec progression (avant → après) - [x] Afficher les liens externes (site live, GitHub) si présents - [x] Ajouter un bouton "Retour à la galerie" - [x] **Task 5: Implémenter la navigation prev/next** (AC: #5) - [x] Ajouter les boutons "Projet précédent" et "Projet suivant" - [x] Utiliser NuxtLink pour la navigation - [x] Afficher le titre du projet dans le bouton - [x] Désactiver/masquer si pas de prev ou next - [x] **Task 6: Meta tags SEO dynamiques** (AC: #7) - [x] Utiliser `useHead()` avec le titre du projet - [x] Utiliser `useSeoMeta()` pour description, og:title, og:description, og:image - [x] L'image OG doit être l'image du projet - [x] **Task 7: Gestion de l'erreur 404** (AC: #8) - [x] Détecter si le projet n'existe pas - [x] Afficher un message approprié avec le narrateur - [x] Proposer de retourner à la galerie - [x] **Task 8: Design responsive** (AC: #9) - [x] Mobile : layout vertical, image pleine largeur - [x] Desktop : layout 2 colonnes (image + contenu) ou grande image + contenu dessous - [x] Liste des compétences responsive (flex wrap) - [x] **Task 9: Tests et validation** - [x] Tester avec différents slugs de projets - [x] Tester la navigation prev/next - [x] Tester le 404 avec un slug inexistant - [x] Valider les meta tags SEO - [x] Tester le responsive ## Dev Notes ### Endpoint API Laravel ```php header('Accept-Language', 'fr'); $project = Project::with('skills') ->where('slug', $slug) ->first(); if (!$project) { return response()->json([ 'error' => [ 'code' => 'PROJECT_NOT_FOUND', 'message' => 'Project not found', ] ], 404); } // Navigation prev/next $allProjects = Project::orderByDesc('is_featured') ->orderByDesc('date_completed') ->get(['id', 'slug', 'title_key']); $currentIndex = $allProjects->search(fn ($p) => $p->slug === $slug); $prev = $currentIndex > 0 ? $allProjects[$currentIndex - 1] : null; $next = $currentIndex < $allProjects->count() - 1 ? $allProjects[$currentIndex + 1] : null; return (new ProjectResource($project))->additional([ 'meta' => [ 'lang' => $lang, ], 'navigation' => [ 'prev' => $prev ? [ 'slug' => $prev->slug, 'title' => Translation::getTranslation($prev->title_key, $lang), ] : null, 'next' => $next ? [ 'slug' => $next->slug, 'title' => Translation::getTranslation($next->title_key, $lang), ] : null, ], ]); } ``` ```php // api/routes/api.php Route::get('/projects/{slug}', [ProjectController::class, 'show']); ``` ### Composable useFetchProject ```typescript // frontend/app/composables/useFetchProject.ts import type { Project } from '~/types/project' interface ProjectNavigation { prev: { slug: string; title: string } | null next: { slug: string; title: string } | null } interface ProjectResponse { data: Project meta: { lang: string } navigation: ProjectNavigation } export function useFetchProject(slug: string | Ref) { const config = useRuntimeConfig() const { locale } = useI18n() const slugValue = toValue(slug) return useFetch(`/projects/${slugValue}`, { baseURL: config.public.apiUrl, headers: { 'X-API-Key': config.public.apiKey, 'Accept-Language': locale.value, }, }) } ``` ### Page [slug].vue ```vue ``` ### Clés i18n nécessaires **fr.json :** ```json { "projects": { "loading": "Chargement...", "notFound": "Projet introuvable", "notFoundDescription": "Ce projet n'existe pas ou a été supprimé.", "backToGallery": "Retour à la galerie", "completedOn": "Réalisé le", "visitSite": "Voir le site", "viewCode": "Voir le code", "skillsUsed": "Compétences utilisées", "skillLevel": "Niveau", "previous": "Projet précédent", "next": "Projet suivant" } } ``` **en.json :** ```json { "projects": { "loading": "Loading...", "notFound": "Project not found", "notFoundDescription": "This project doesn't exist or has been removed.", "backToGallery": "Back to gallery", "completedOn": "Completed on", "visitSite": "Visit site", "viewCode": "View code", "skillsUsed": "Skills used", "skillLevel": "Level", "previous": "Previous project", "next": "Next project" } } ``` ### Configuration i18n pour les dates Ajouter dans `nuxt.config.ts` la configuration des formats de date : ```typescript i18n: { datetimeFormats: { fr: { long: { year: 'numeric', month: 'long', day: 'numeric' } }, en: { long: { year: 'numeric', month: 'long', day: 'numeric' } } } } ``` ### Dépendances **Cette story nécessite :** - Story 2.1 : Composant ProjectCard - Story 2.2 : Endpoint API `/api/projects` et page galerie **Cette story prépare pour :** - Story 2.5 : Compétences cliquables (liens vers projets) ### Project Structure Notes **Fichiers à créer :** ``` frontend/app/ ├── pages/ │ └── projets/ │ └── [slug].vue # CRÉER └── composables/ └── useFetchProject.ts # CRÉER ``` **Fichiers à modifier :** ``` api/app/Http/Controllers/Api/ProjectController.php # AJOUTER show() api/routes/api.php # AJOUTER route frontend/i18n/fr.json # AJOUTER clés frontend/i18n/en.json # AJOUTER clés frontend/nuxt.config.ts # AJOUTER datetimeFormats ``` ### References - [Source: docs/planning-artifacts/epics.md#Story-2.3] - [Source: docs/planning-artifacts/architecture.md#API-&-Communication-Patterns] - [Source: docs/planning-artifacts/ux-design-specification.md#Responsive-Strategy] ### Technical Requirements | Requirement | Value | Source | |-------------|-------|--------| | Route dynamique | /projets/[slug] | Nuxt routing | | API endpoint | GET /api/projects/{slug} | Architecture | | Navigation | prev/next avec titres | Epics | | SEO | Meta dynamiques + OG image | NFR5 | | 404 | Message approprié | Epics | ## Dev Agent Record ### Agent Model Used Claude Opus 4.5 (claude-opus-4-5-20251101) ### Debug Log References - Aucun problème. Méthode show() existait déjà mais sans navigation prev/next. ### Completion Notes List - ProjectController show() amélioré avec navigation prev/next et erreur 404 structurée - Composable useFetchProject créé avec typage ProjectNavigation - Page [slug].vue complète avec : - Image principale pleine largeur - Titre, date formatée selon locale, badge featured - Description complète - Liens externes (site et GitHub) - Grille responsive des compétences utilisées avec niveaux avant/après - Navigation prev/next avec titres traduits - État loading (skeleton), état 404 avec narrateur spider - SEO dynamique via useSeo avec og:image du projet - Formatage date via Intl.DateTimeFormat selon locale - Traductions FR/EN ajoutées (10 nouvelles clés projects.*) - Store progression visitSection('projets') sur mount ### Change Log | Date | Change | Author | |------|--------|--------| | 2026-02-04 | Story créée avec contexte complet | SM Agent | | 2026-02-06 | Tasks 1-9 implémentées et validées | Dev Agent (Claude Opus 4.5) | ### File List - `api/app/Http/Controllers/Api/ProjectController.php` — MODIFIÉ (show avec navigation) - `frontend/app/composables/useFetchProject.ts` — CRÉÉ - `frontend/app/pages/projets/[slug].vue` — RÉÉCRIT - `frontend/i18n/fr.json` — MODIFIÉ (ajout projects.*) - `frontend/i18n/en.json` — MODIFIÉ (ajout projects.*)