From 0399f0dc1c346941408be40e0345d2222e96faca Mon Sep 17 00:00:00 2001 From: skycel Date: Fri, 6 Feb 2026 02:12:32 +0100 Subject: [PATCH] :sparkles: Add projects gallery page with responsive grid (Story 2.2) - Create useFetchProjects composable for API integration - Implement responsive grid layout (1/2/3/4 columns) - Add stagger fadeInUp animation with prefers-reduced-motion support - Include loading skeleton, error state with retry, and empty state - Configure SEO meta tags via useSeo composable - Update Project model ordered scope for proper sorting Co-Authored-By: Claude Opus 4.5 --- api/app/Models/Project.php | 4 +- .../2-2-page-projets-galerie.md | 107 +++++++++++------- frontend/app/composables/useFetchProjects.ts | 21 ++++ frontend/app/pages/projets/index.vue | 93 ++++++++++++++- frontend/i18n/en.json | 7 +- frontend/i18n/fr.json | 11 +- 6 files changed, 188 insertions(+), 55 deletions(-) create mode 100644 frontend/app/composables/useFetchProjects.ts diff --git a/api/app/Models/Project.php b/api/app/Models/Project.php index 90995d6..88e4085 100644 --- a/api/app/Models/Project.php +++ b/api/app/Models/Project.php @@ -45,6 +45,8 @@ class Project extends Model public function scopeOrdered(Builder $query): Builder { - return $query->orderBy('display_order'); + return $query->orderByDesc('is_featured') + ->orderByDesc('date_completed') + ->orderBy('display_order'); } } diff --git a/docs/implementation-artifacts/2-2-page-projets-galerie.md b/docs/implementation-artifacts/2-2-page-projets-galerie.md index ecb6156..3521daf 100644 --- a/docs/implementation-artifacts/2-2-page-projets-galerie.md +++ b/docs/implementation-artifacts/2-2-page-projets-galerie.md @@ -1,6 +1,6 @@ # Story 2.2: Page Projets - Galerie -Status: ready-for-dev +Status: review ## Story @@ -19,55 +19,55 @@ so that je peux évaluer son expérience et choisir lesquels explorer en détail ## Tasks / Subtasks -- [ ] **Task 1: Créer l'endpoint API Laravel** (AC: #4) - - [ ] Créer `app/Http/Controllers/Api/ProjectController.php` - - [ ] Créer la méthode `index()` pour lister tous les projets - - [ ] Implémenter le tri : featured en premier, puis par date_completed DESC - - [ ] Joindre les traductions selon le header `Accept-Language` - - [ ] Créer `app/Http/Resources/ProjectResource.php` pour formater la réponse - - [ ] Ajouter la route `GET /api/projects` dans `routes/api.php` +- [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` -- [ ] **Task 2: Créer le composable useFetchProjects** (AC: #4) - - [ ] Créer `frontend/app/composables/useFetchProjects.ts` - - [ ] Utiliser `useFetch()` pour appeler l'API avec le header `Accept-Language` - - [ ] Gérer les états loading, error, data - - [ ] Typer la réponse avec l'interface Project[] +- [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[] -- [ ] **Task 3: Créer la page projets.vue** (AC: #1, #6) - - [ ] Créer `frontend/app/pages/projets.vue` - - [ ] Utiliser le composable `useFetchProjects()` pour charger les données - - [ ] Afficher une grille de `ProjectCard` avec les données - - [ ] Implémenter le layout responsive : 1 colonne mobile, 2 tablette, 3-4 desktop +- [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 -- [ ] **Task 4: Implémenter l'animation d'entrée** (AC: #3) - - [ ] Animer l'apparition progressive des cards (stagger animation) - - [ ] Utiliser CSS animations ou GSAP pour un effet fade-in + slide-up - - [ ] Respecter `prefers-reduced-motion` : pas d'animation si activé - - [ ] Délai de 50-100ms entre chaque card +- [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 -- [ ] **Task 5: Tri des projets** (AC: #2) - - [ ] S'assurer que l'API retourne les projets dans le bon ordre - - [ ] Vérifier côté frontend que l'ordre est respecté - - [ ] Les projets `is_featured: true` apparaissent en premier - - [ ] Puis tri par `date_completed` DESC +- [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 -- [ ] **Task 6: Meta tags SEO** (AC: #5) - - [ ] Utiliser `useHead()` pour définir le titre dynamique - - [ ] Utiliser `useSeoMeta()` pour les meta description, og:title, og:description - - [ ] Ajouter les clés i18n pour titre et description de la page - - [ ] Exemple titre : "Projets | Skycel" / "Projects | Skycel" +- [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" -- [ ] **Task 7: État loading et erreur** - - [ ] Afficher un skeleton/loading state pendant le chargement - - [ ] Afficher un message d'erreur narratif si l'API échoue - - [ ] Bouton "Réessayer" en cas d'erreur +- [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 -- [ ] **Task 8: Tests et validation** - - [ ] Tester la page en FR et EN - - [ ] Vérifier le tri des projets - - [ ] Tester l'animation d'entrée - - [ ] Valider le responsive sur mobile/tablette/desktop - - [ ] Vérifier les meta tags avec l'inspecteur +- [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 @@ -372,16 +372,35 @@ frontend/i18n/en.json # AJOUTER clés projects.* ### Agent Model Used -{{agent_model_name_version}} +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) + diff --git a/frontend/app/composables/useFetchProjects.ts b/frontend/app/composables/useFetchProjects.ts new file mode 100644 index 0000000..9e1a9ce --- /dev/null +++ b/frontend/app/composables/useFetchProjects.ts @@ -0,0 +1,21 @@ +import type { Project } from '~/types/project' + +interface ProjectsResponse { + data: Project[] + meta: { lang: string } +} + +export function useFetchProjects() { + const config = useRuntimeConfig() + const { locale } = useI18n() + + return useFetch('/projects', { + baseURL: config.public.apiUrl as string, + headers: { + 'X-API-Key': config.public.apiKey as string, + 'Accept-Language': locale.value, + }, + transform: (response) => response.data, + default: () => [], + }) +} diff --git a/frontend/app/pages/projets/index.vue b/frontend/app/pages/projets/index.vue index 830b479..5218a88 100644 --- a/frontend/app/pages/projets/index.vue +++ b/frontend/app/pages/projets/index.vue @@ -1,16 +1,97 @@ + + diff --git a/frontend/i18n/en.json b/frontend/i18n/en.json index bbe5fd4..9bf7885 100644 --- a/frontend/i18n/en.json +++ b/frontend/i18n/en.json @@ -16,7 +16,8 @@ "loading": "Loading...", "language": "Language", "back_home": "Back to home", - "back_to_adventure": "Back to the adventure" + "back_to_adventure": "Back to the adventure", + "retry": "Retry" }, "landing": { "title": "Welcome to my universe", @@ -75,8 +76,12 @@ "meta_description": "Full-Stack Developer specialized in Vue.js, Nuxt, Laravel. Discover my profile and projects in 30 seconds." }, "projects": { + "title": "My Projects", + "page_title": "Projects | Skycel", + "page_description": "Discover projects created by C\u00e9lian, full-stack web developer.", "discover": "Discover", "no_projects": "No projects yet", + "load_error": "Unable to load projects...", "view_all": "View all projects" }, "pages": { diff --git a/frontend/i18n/fr.json b/frontend/i18n/fr.json index 682d3e4..abd8265 100644 --- a/frontend/i18n/fr.json +++ b/frontend/i18n/fr.json @@ -11,12 +11,13 @@ "common": { "continue": "Continuer", "back": "Retour", - "discover": "Découvrir", + "discover": "D\u00e9couvrir", "close": "Fermer", "loading": "Chargement...", "language": "Langue", - "back_home": "Retour à l'accueil", - "back_to_adventure": "Retour à l'aventure" + "back_home": "Retour \u00e0 l'accueil", + "back_to_adventure": "Retour \u00e0 l'aventure", + "retry": "R\u00e9essayer" }, "landing": { "title": "Bienvenue dans mon univers", @@ -75,8 +76,12 @@ "meta_description": "D\u00e9veloppeur Full-Stack sp\u00e9cialis\u00e9 en Vue.js, Nuxt, Laravel. D\u00e9couvrez mon profil et mes projets en 30 secondes." }, "projects": { + "title": "Mes Projets", + "page_title": "Projets | Skycel", + "page_description": "D\u00e9couvrez les projets r\u00e9alis\u00e9s par C\u00e9lian, d\u00e9veloppeur web full-stack.", "discover": "D\u00e9couvrir", "no_projects": "Aucun projet pour le moment", + "load_error": "Impossible de charger les projets...", "view_all": "Voir tous les projets" }, "pages": {