diff --git a/api/app/Http/Controllers/Api/ProjectController.php b/api/app/Http/Controllers/Api/ProjectController.php index 497bfd4..e1cc35b 100644 --- a/api/app/Http/Controllers/Api/ProjectController.php +++ b/api/app/Http/Controllers/Api/ProjectController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Http\Resources\ProjectResource; use App\Models\Project; +use App\Models\Translation; class ProjectController extends Controller { @@ -18,9 +19,39 @@ class ProjectController extends Controller public function show(string $slug) { - $project = Project::with('skills')->where('slug', $slug)->firstOrFail(); + $project = Project::with('skills')->where('slug', $slug)->first(); + + if (!$project) { + return response()->json([ + 'error' => [ + 'code' => 'PROJECT_NOT_FOUND', + 'message' => 'Project not found', + ], + ], 404); + } + + $lang = app()->getLocale(); + + // Get all projects in order for navigation + $allProjects = Project::ordered()->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' => app()->getLocale()]]); + ->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, + ], + ]); } } diff --git a/docs/implementation-artifacts/2-3-page-projet-detail.md b/docs/implementation-artifacts/2-3-page-projet-detail.md index d0b6944..e6f6398 100644 --- a/docs/implementation-artifacts/2-3-page-projet-detail.md +++ b/docs/implementation-artifacts/2-3-page-projet-detail.md @@ -1,6 +1,6 @@ # Story 2.3: Page Projet - Détail -Status: ready-for-dev +Status: review ## Story @@ -22,59 +22,59 @@ so that je comprends le travail réalisé et les technologies utilisées. ## Tasks / Subtasks -- [ ] **Task 1: Créer l'endpoint API pour le détail du projet** (AC: #1, #2, #3, #4, #8) - - [ ] Ajouter la méthode `show($slug)` dans `ProjectController` - - [ ] Charger le projet avec ses compétences (eager loading) - - [ ] Retourner 404 si le slug n'existe pas - - [ ] Inclure les données de traduction selon `Accept-Language` +- [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` -- [ ] **Task 2: Créer l'endpoint API pour la navigation prev/next** (AC: #5) - - [ ] Ajouter une méthode `navigation($slug)` ou inclure dans `show()` - - [ ] Retourner le projet précédent et suivant (basé sur l'ordre de tri) - - [ ] Si premier projet : prev = null, si dernier : next = null +- [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 -- [ ] **Task 3: Créer le composable useFetchProject** (AC: #1) - - [ ] Créer `frontend/app/composables/useFetchProject.ts` - - [ ] Accepter le slug en paramètre - - [ ] Gérer les états loading, error, data - - [ ] Gérer l'erreur 404 +- [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 -- [ ] **Task 4: Créer la page [slug].vue** (AC: #1, #2, #3, #4, #6, #9) - - [ ] Créer `frontend/app/pages/projets/[slug].vue` - - [ ] Afficher l'image principale en grand format - - [ ] Afficher le titre et la description complète - - [ ] Afficher la date de réalisation formatée - - [ ] Afficher la liste des compétences avec progression (avant → après) - - [ ] Afficher les liens externes (site live, GitHub) si présents - - [ ] Ajouter un bouton "Retour à la galerie" +- [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" -- [ ] **Task 5: Implémenter la navigation prev/next** (AC: #5) - - [ ] Ajouter les boutons "Projet précédent" et "Projet suivant" - - [ ] Utiliser NuxtLink pour la navigation - - [ ] Afficher le titre du projet dans le bouton - - [ ] Désactiver/masquer si pas de prev ou next +- [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 -- [ ] **Task 6: Meta tags SEO dynamiques** (AC: #7) - - [ ] Utiliser `useHead()` avec le titre du projet - - [ ] Utiliser `useSeoMeta()` pour description, og:title, og:description, og:image - - [ ] L'image OG doit être l'image du projet +- [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 -- [ ] **Task 7: Gestion de l'erreur 404** (AC: #8) - - [ ] Détecter si le projet n'existe pas - - [ ] Afficher un message approprié avec le narrateur - - [ ] Proposer de retourner à la galerie +- [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 -- [ ] **Task 8: Design responsive** (AC: #9) - - [ ] Mobile : layout vertical, image pleine largeur - - [ ] Desktop : layout 2 colonnes (image + contenu) ou grande image + contenu dessous - - [ ] Liste des compétences responsive (flex wrap) +- [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) -- [ ] **Task 9: Tests et validation** - - [ ] Tester avec différents slugs de projets - - [ ] Tester la navigation prev/next - - [ ] Tester le 404 avec un slug inexistant - - [ ] Valider les meta tags SEO - - [ ] Tester le responsive +- [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 @@ -437,16 +437,40 @@ frontend/nuxt.config.ts # AJOUTER datetimeFormats ### Agent Model Used -{{agent_model_name_version}} +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.*) + diff --git a/docs/implementation-artifacts/sprint-status.yaml b/docs/implementation-artifacts/sprint-status.yaml index 470bbbe..b7a5871 100644 --- a/docs/implementation-artifacts/sprint-status.yaml +++ b/docs/implementation-artifacts/sprint-status.yaml @@ -58,8 +58,8 @@ development_status: # ═══════════════════════════════════════════════════════════════════════════ epic-2: in-progress 2-1-composant-projectcard: review - 2-2-page-projets-galerie: ready-for-dev - 2-3-page-projet-detail: ready-for-dev + 2-2-page-projets-galerie: review + 2-3-page-projet-detail: review 2-4-page-competences-affichage-categories: ready-for-dev 2-5-competences-cliquables-projets-lies: ready-for-dev 2-6-page-temoignages-migrations-bdd: ready-for-dev diff --git a/frontend/app/composables/useFetchProject.ts b/frontend/app/composables/useFetchProject.ts new file mode 100644 index 0000000..22de51f --- /dev/null +++ b/frontend/app/composables/useFetchProject.ts @@ -0,0 +1,33 @@ +import type { Project } from '~/types/project' + +export interface ProjectNavigation { + prev: { slug: string; title: string } | null + next: { slug: string; title: string } | null +} + +interface ProjectResponse { + data: Project + meta: { lang: string } + navigation: ProjectNavigation +} + +interface ProjectError { + error: { + code: string + message: string + } +} + +export function useFetchProject(slug: string | Ref) { + const config = useRuntimeConfig() + const { locale } = useI18n() + const slugValue = toValue(slug) + + return useFetch(`/projects/${slugValue}`, { + baseURL: config.public.apiUrl as string, + headers: { + 'X-API-Key': config.public.apiKey as string, + 'Accept-Language': locale.value, + }, + }) +} diff --git a/frontend/app/pages/projets/[slug].vue b/frontend/app/pages/projets/[slug].vue index 7c04924..5247f27 100644 --- a/frontend/app/pages/projets/[slug].vue +++ b/frontend/app/pages/projets/[slug].vue @@ -1,19 +1,205 @@ diff --git a/frontend/i18n/en.json b/frontend/i18n/en.json index 9bf7885..19ef7ab 100644 --- a/frontend/i18n/en.json +++ b/frontend/i18n/en.json @@ -82,7 +82,17 @@ "discover": "Discover", "no_projects": "No projects yet", "load_error": "Unable to load projects...", - "view_all": "View all projects" + "view_all": "View all projects", + "not_found": "Project not found", + "not_found_description": "This project doesn't exist or has been removed.", + "back_to_gallery": "Back to gallery", + "completed_on": "Completed on", + "visit_site": "Visit site", + "view_code": "View code", + "skills_used": "Skills used", + "skill_level": "Level", + "previous": "Previous project", + "next": "Next project" }, "pages": { "projects": { diff --git a/frontend/i18n/fr.json b/frontend/i18n/fr.json index abd8265..c99f3b6 100644 --- a/frontend/i18n/fr.json +++ b/frontend/i18n/fr.json @@ -82,7 +82,17 @@ "discover": "D\u00e9couvrir", "no_projects": "Aucun projet pour le moment", "load_error": "Impossible de charger les projets...", - "view_all": "Voir tous les projets" + "view_all": "Voir tous les projets", + "not_found": "Projet introuvable", + "not_found_description": "Ce projet n'existe pas ou a \u00e9t\u00e9 supprim\u00e9.", + "back_to_gallery": "Retour \u00e0 la galerie", + "completed_on": "R\u00e9alis\u00e9 le", + "visit_site": "Voir le site", + "view_code": "Voir le code", + "skills_used": "Comp\u00e9tences utilis\u00e9es", + "skill_level": "Niveau", + "previous": "Projet pr\u00e9c\u00e9dent", + "next": "Projet suivant" }, "pages": { "projects": {