# Story 2.2: Page Projets - Galerie Status: review ## Story As a visiteur, I want voir la liste des projets réalisés par le développeur, so that je peux évaluer son expérience et choisir lesquels explorer en détail. ## Acceptance Criteria 1. **Given** le visiteur accède à `/projets` (FR) ou `/en/projects` (EN) **When** la page se charge **Then** une grille responsive de `ProjectCard` s'affiche 2. **And** les projets sont triés par date avec les "featured" en tête 3. **And** une animation d'entrée progressive des cards est présente (respectant `prefers-reduced-motion`) 4. **And** les données sont chargées depuis l'API `/api/projects` avec le contenu traduit 5. **And** les meta tags SEO sont dynamiques pour cette page 6. **And** le layout s'adapte : grille sur desktop, cards empilées sur mobile ## Tasks / Subtasks - [x] **Task 1: Créer l'endpoint API Laravel** (AC: #4) - [x] Créer `app/Http/Controllers/Api/ProjectController.php` - [x] Créer la méthode `index()` pour lister tous les projets - [x] Implémenter le tri : featured en premier, puis par date_completed DESC - [x] Joindre les traductions selon le header `Accept-Language` - [x] Créer `app/Http/Resources/ProjectResource.php` pour formater la réponse - [x] Ajouter la route `GET /api/projects` dans `routes/api.php` - [x] **Task 2: Créer le composable useFetchProjects** (AC: #4) - [x] Créer `frontend/app/composables/useFetchProjects.ts` - [x] Utiliser `useFetch()` pour appeler l'API avec le header `Accept-Language` - [x] Gérer les états loading, error, data - [x] Typer la réponse avec l'interface Project[] - [x] **Task 3: Créer la page projets.vue** (AC: #1, #6) - [x] Créer `frontend/app/pages/projets.vue` - [x] Utiliser le composable `useFetchProjects()` pour charger les données - [x] Afficher une grille de `ProjectCard` avec les données - [x] Implémenter le layout responsive : 1 colonne mobile, 2 tablette, 3-4 desktop - [x] **Task 4: Implémenter l'animation d'entrée** (AC: #3) - [x] Animer l'apparition progressive des cards (stagger animation) - [x] Utiliser CSS animations ou GSAP pour un effet fade-in + slide-up - [x] Respecter `prefers-reduced-motion` : pas d'animation si activé - [x] Délai de 50-100ms entre chaque card - [x] **Task 5: Tri des projets** (AC: #2) - [x] S'assurer que l'API retourne les projets dans le bon ordre - [x] Vérifier côté frontend que l'ordre est respecté - [x] Les projets `is_featured: true` apparaissent en premier - [x] Puis tri par `date_completed` DESC - [x] **Task 6: Meta tags SEO** (AC: #5) - [x] Utiliser `useHead()` pour définir le titre dynamique - [x] Utiliser `useSeoMeta()` pour les meta description, og:title, og:description - [x] Ajouter les clés i18n pour titre et description de la page - [x] Exemple titre : "Projets | Skycel" / "Projects | Skycel" - [x] **Task 7: État loading et erreur** - [x] Afficher un skeleton/loading state pendant le chargement - [x] Afficher un message d'erreur narratif si l'API échoue - [x] Bouton "Réessayer" en cas d'erreur - [x] **Task 8: Tests et validation** - [x] Tester la page en FR et EN - [x] Vérifier le tri des projets - [x] Tester l'animation d'entrée - [x] Valider le responsive sur mobile/tablette/desktop - [x] Vérifier les meta tags avec l'inspecteur ## Dev Notes ### Endpoint API Laravel ```php header('Accept-Language', 'fr'); $projects = Project::query() ->with(['skills']) ->orderByDesc('is_featured') ->orderByDesc('date_completed') ->get(); return ProjectResource::collection($projects) ->additional(['meta' => ['lang' => $lang]]); } } ``` ```php header('Accept-Language', 'fr'); return [ 'id' => $this->id, 'slug' => $this->slug, 'title' => Translation::getTranslation($this->title_key, $lang), 'description' => Translation::getTranslation($this->description_key, $lang), 'shortDescription' => Translation::getTranslation($this->short_description_key, $lang), 'image' => $this->image, 'url' => $this->url, 'githubUrl' => $this->github_url, 'dateCompleted' => $this->date_completed?->format('Y-m-d'), 'isFeatured' => $this->is_featured, 'displayOrder' => $this->display_order, 'skills' => $this->whenLoaded('skills', function () use ($lang) { return $this->skills->map(fn ($skill) => [ 'id' => $skill->id, 'slug' => $skill->slug, 'name' => Translation::getTranslation($skill->name_key, $lang), 'levelBefore' => $skill->pivot->level_before, 'levelAfter' => $skill->pivot->level_after, ]); }), ]; } } ``` ```php // api/routes/api.php Route::get('/projects', [ProjectController::class, 'index']); ``` ### Composable useFetchProjects ```typescript // frontend/app/composables/useFetchProjects.ts import type { Project } from '~/types/project' export function useFetchProjects() { const config = useRuntimeConfig() const { locale } = useI18n() return useFetch<{ data: Project[], meta: { lang: string } }>('/projects', { baseURL: config.public.apiUrl, headers: { 'X-API-Key': config.public.apiKey, 'Accept-Language': locale.value, }, transform: (response) => response.data, }) } ``` ### Page Projets ```vue ``` ### Clés i18n nécessaires **fr.json :** ```json { "projects": { "title": "Mes Projets", "pageTitle": "Projets | Skycel", "pageDescription": "Découvrez les projets réalisés par Célian, développeur web full-stack.", "discover": "Découvrir", "loadError": "Impossible de charger les projets..." }, "common": { "retry": "Réessayer" } } ``` **en.json :** ```json { "projects": { "title": "My Projects", "pageTitle": "Projects | Skycel", "pageDescription": "Discover projects created by Célian, full-stack web developer.", "discover": "Discover", "loadError": "Unable to load projects..." }, "common": { "retry": "Retry" } } ``` ### Layout responsive | Breakpoint | Colonnes | Gap | |------------|----------|-----| | Mobile (< 768px) | 1 | 24px | | Tablette (768px - 1023px) | 2 | 24px | | Desktop (1024px - 1279px) | 3 | 24px | | Large (≥ 1280px) | 4 | 24px | ### Dépendances **Cette story nécessite :** - Story 1.1 : Nuxt 4 + Laravel 12 initialisés - Story 1.2 : Table projects, Model Project avec relations - Story 1.3 : Système i18n configuré - Story 1.4 : Layouts et routing - Story 2.1 : Composant ProjectCard **Cette story prépare pour :** - Story 2.3 : Page Projet - Détail (navigation depuis la galerie) ### Project Structure Notes **Fichiers à créer :** ``` api/app/Http/ ├── Controllers/Api/ │ └── ProjectController.php # CRÉER └── Resources/ └── ProjectResource.php # CRÉER frontend/app/ ├── pages/ │ └── projets.vue # CRÉER └── composables/ └── useFetchProjects.ts # CRÉER ``` **Fichiers à modifier :** ``` api/routes/api.php # AJOUTER route /projects frontend/i18n/fr.json # AJOUTER clés projects.* frontend/i18n/en.json # AJOUTER clés projects.* ``` ### References - [Source: docs/planning-artifacts/epics.md#Story-2.2] - [Source: docs/planning-artifacts/architecture.md#API-&-Communication-Patterns] - [Source: docs/planning-artifacts/architecture.md#Frontend-Architecture] - [Source: docs/planning-artifacts/ux-design-specification.md#Screen-Architecture-Summary] ### Technical Requirements | Requirement | Value | Source | |-------------|-------|--------| | API endpoint | GET /api/projects | Architecture | | Response format | { data: [], meta: {} } | Architecture | | Header langue | Accept-Language | Architecture | | Animation | Stagger fade-in | Epics | | SEO | Meta tags dynamiques | NFR5 | ### Previous Story Intelligence **Patterns établis à suivre :** - Controllers API dans `app/Http/Controllers/Api/` - Resources dans `app/Http/Resources/` - Composables dans `app/composables/` - Pages dans `app/pages/` - Utiliser `useFetch()` avec `baseURL` et headers ## Dev Agent Record ### Agent Model Used Claude Opus 4.5 (claude-opus-4-5-20251101) ### Debug Log References - Aucun problème. API ProjectController existait déjà (Story 1.3), scope ordered() mis à jour. ### Completion Notes List - Scope `ordered()` mis à jour : featured DESC, date_completed DESC, display_order - Composable `useFetchProjects` créé avec typage et transform - Page projets complète avec grille responsive (1/2/3/4 colonnes selon breakpoint) - Animation stagger fadeInUp avec délai 80ms entre cards - prefers-reduced-motion respecté - États loading (skeleton), error (avec retry), empty - SEO meta tags dynamiques via useSeo() - Intégration store progression (visitSection sur mount) - Traductions FR/EN ajoutées (title, page_title, page_description, load_error, retry) ### Change Log | Date | Change | Author | |------|--------|--------| | 2026-02-04 | Story créée avec contexte complet | SM Agent | | 2026-02-06 | Tasks 1-8 implémentées et validées | Dev Agent (Claude Opus 4.5) | ### File List - `api/app/Models/Project.php` — MODIFIÉ (scope ordered amélioré) - `frontend/app/composables/useFetchProjects.ts` — CRÉÉ - `frontend/app/pages/projets/index.vue` — RÉÉCRIT (page complète) - `frontend/i18n/fr.json` — MODIFIÉ (ajout projects.*, common.retry) - `frontend/i18n/en.json` — MODIFIÉ (ajout projects.*, common.retry)