From 4db96a0ded9c90205bfd844de18481b58861a2f4 Mon Sep 17 00:00:00 2001 From: skycel Date: Fri, 6 Feb 2026 10:37:10 +0100 Subject: [PATCH] :sparkles: Add skills page with category grouping (Story 2.4) - Enhance Skill model with getCurrentLevel() and ordered scope - Update SkillController to group by category with translated labels - Add level and project_count to SkillResource - Create skill.ts types (Skill, SkillCategory, SkillsResponse) - Create useFetchSkills composable - Create SkillCard component with animated progress bar - Implement competences.vue with: - Responsive grid (2/3/4 columns) - Category sections with icons - Stagger animations (respects prefers-reduced-motion) - Loading/error/empty states - Placeholder for vis.js skill tree (Epic 3) Co-Authored-By: Claude Opus 4.5 --- .../Http/Controllers/Api/SkillController.php | 31 +++- api/app/Http/Resources/SkillResource.php | 3 + api/app/Models/Skill.php | 9 +- ...4-page-competences-affichage-categories.md | 128 +++++++++------ .../sprint-status.yaml | 2 +- frontend/app/components/feature/SkillCard.vue | 83 ++++++++++ frontend/app/composables/useFetchSkills.ts | 14 ++ frontend/app/pages/competences.vue | 151 +++++++++++++++++- frontend/app/types/skill.ts | 30 ++++ frontend/i18n/en.json | 11 ++ frontend/i18n/fr.json | 17 +- 11 files changed, 414 insertions(+), 65 deletions(-) create mode 100644 frontend/app/components/feature/SkillCard.vue create mode 100644 frontend/app/composables/useFetchSkills.ts create mode 100644 frontend/app/types/skill.ts diff --git a/api/app/Http/Controllers/Api/SkillController.php b/api/app/Http/Controllers/Api/SkillController.php index 8763c1a..aedc50b 100644 --- a/api/app/Http/Controllers/Api/SkillController.php +++ b/api/app/Http/Controllers/Api/SkillController.php @@ -10,13 +10,36 @@ class SkillController extends Controller { public function index() { - $skills = Skill::ordered()->get()->groupBy('category'); + $lang = app()->getLocale(); + $skills = Skill::with('projects')->ordered()->get(); + $grouped = $skills->groupBy('category'); - $grouped = $skills->map(fn ($group) => SkillResource::collection($group)); + $data = $grouped->map(function ($categorySkills, $category) use ($lang) { + return [ + 'category' => $category, + 'category_label' => $this->getCategoryLabel($category, $lang), + 'skills' => SkillResource::collection($categorySkills), + ]; + })->values(); return response()->json([ - 'data' => $grouped, - 'meta' => ['lang' => app()->getLocale()], + 'data' => $data, + 'meta' => [ + 'lang' => $lang, + 'total' => $skills->count(), + ], ]); } + + private function getCategoryLabel(string $category, string $lang): string + { + $labels = [ + 'frontend' => ['fr' => 'Frontend', 'en' => 'Frontend'], + 'backend' => ['fr' => 'Backend', 'en' => 'Backend'], + 'tools' => ['fr' => 'Outils', 'en' => 'Tools'], + 'soft_skills' => ['fr' => 'Soft Skills', 'en' => 'Soft Skills'], + ]; + + return $labels[strtolower($category)][$lang] ?? ucfirst($category); + } } diff --git a/api/app/Http/Resources/SkillResource.php b/api/app/Http/Resources/SkillResource.php index 6f2f80f..fb43f12 100644 --- a/api/app/Http/Resources/SkillResource.php +++ b/api/app/Http/Resources/SkillResource.php @@ -16,7 +16,10 @@ class SkillResource extends JsonResource 'description' => $this->getTranslated('description_key'), 'icon' => $this->icon, 'category' => $this->category, + 'level' => $this->getCurrentLevel(), 'max_level' => $this->max_level, + 'display_order' => $this->display_order, + 'project_count' => $this->whenLoaded('projects', fn () => $this->projects->count()), 'pivot' => $this->when($this->pivot, fn () => [ 'level_before' => $this->pivot->level_before, 'level_after' => $this->pivot->level_after, diff --git a/api/app/Models/Skill.php b/api/app/Models/Skill.php index 4f6179b..0a8f63a 100644 --- a/api/app/Models/Skill.php +++ b/api/app/Models/Skill.php @@ -34,6 +34,13 @@ class Skill extends Model public function scopeOrdered(Builder $query): Builder { - return $query->orderBy('display_order'); + return $query->orderBy('category')->orderBy('display_order'); + } + + public function getCurrentLevel(): int + { + $maxLevelAfter = $this->projects()->max('skill_project.level_after'); + + return $maxLevelAfter ?? 1; } } diff --git a/docs/implementation-artifacts/2-4-page-competences-affichage-categories.md b/docs/implementation-artifacts/2-4-page-competences-affichage-categories.md index 96f789e..24ac477 100644 --- a/docs/implementation-artifacts/2-4-page-competences-affichage-categories.md +++ b/docs/implementation-artifacts/2-4-page-competences-affichage-categories.md @@ -1,6 +1,6 @@ # Story 2.4: Page Compétences - Affichage par catégories -Status: ready-for-dev +Status: review ## Story @@ -21,61 +21,61 @@ so that je comprends son profil technique global. ## Tasks / Subtasks -- [ ] **Task 1: Créer l'endpoint API Laravel pour les skills** (AC: #3) - - [ ] Créer `app/Http/Controllers/Api/SkillController.php` - - [ ] Créer la méthode `index()` pour lister toutes les compétences - - [ ] Grouper les compétences par catégorie - - [ ] Joindre les traductions selon `Accept-Language` - - [ ] Créer `app/Http/Resources/SkillResource.php` - - [ ] Ajouter la route `GET /api/skills` dans `routes/api.php` +- [x] **Task 1: Créer l'endpoint API Laravel pour les skills** (AC: #3) + - [x] Créer `app/Http/Controllers/Api/SkillController.php` + - [x] Créer la méthode `index()` pour lister toutes les compétences + - [x] Grouper les compétences par catégorie + - [x] Joindre les traductions selon `Accept-Language` + - [x] Créer `app/Http/Resources/SkillResource.php` + - [x] Ajouter la route `GET /api/skills` dans `routes/api.php` -- [ ] **Task 2: Créer le composable useFetchSkills** (AC: #3) - - [ ] Créer `frontend/app/composables/useFetchSkills.ts` - - [ ] Gérer les états loading, error, data - - [ ] Typer la réponse avec interface Skill[] +- [x] **Task 2: Créer le composable useFetchSkills** (AC: #3) + - [x] Créer `frontend/app/composables/useFetchSkills.ts` + - [x] Gérer les états loading, error, data + - [x] Typer la réponse avec interface Skill[] -- [ ] **Task 3: Créer le composant SkillCard** (AC: #2, #8) - - [ ] Créer `frontend/app/components/feature/SkillCard.vue` - - [ ] Props : skill (avec name, icon, level, maxLevel) - - [ ] Afficher l'icône (si présente) ou un placeholder - - [ ] Afficher le nom traduit - - [ ] Afficher le niveau avec une barre de progression - - [ ] Style cliquable (hover effect, cursor pointer) +- [x] **Task 3: Créer le composant SkillCard** (AC: #2, #8) + - [x] Créer `frontend/app/components/feature/SkillCard.vue` + - [x] Props : skill (avec name, icon, level, maxLevel) + - [x] Afficher l'icône (si présente) ou un placeholder + - [x] Afficher le nom traduit + - [x] Afficher le niveau avec une barre de progression + - [x] Style cliquable (hover effect, cursor pointer) -- [ ] **Task 4: Créer la page competences.vue** (AC: #1, #6) - - [ ] Créer `frontend/app/pages/competences.vue` - - [ ] Charger les données avec `useFetchSkills()` - - [ ] Grouper les skills par catégorie côté frontend - - [ ] Afficher chaque catégorie comme une section avec titre - - [ ] Grille de SkillCard dans chaque section +- [x] **Task 4: Créer la page competences.vue** (AC: #1, #6) + - [x] Créer `frontend/app/pages/competences.vue` + - [x] Charger les données avec `useFetchSkills()` + - [x] Grouper les skills par catégorie côté frontend + - [x] Afficher chaque catégorie comme une section avec titre + - [x] Grille de SkillCard dans chaque section -- [ ] **Task 5: Implémenter l'animation d'entrée** (AC: #4) - - [ ] Animation stagger pour les SkillCards (comme ProjectCard) - - [ ] Animation fade-in pour les titres de catégories - - [ ] Respecter `prefers-reduced-motion` +- [x] **Task 5: Implémenter l'animation d'entrée** (AC: #4) + - [x] Animation stagger pour les SkillCards (comme ProjectCard) + - [x] Animation fade-in pour les titres de catégories + - [x] Respecter `prefers-reduced-motion` -- [ ] **Task 6: Design responsive** (AC: #5, #6) - - [ ] Mobile : 2 colonnes de SkillCards par catégorie - - [ ] Desktop : 4 colonnes, espace réservé pour vis.js (Epic 3) - - [ ] Catégories empilées verticalement +- [x] **Task 6: Design responsive** (AC: #5, #6) + - [x] Mobile : 2 colonnes de SkillCards par catégorie + - [x] Desktop : 4 colonnes, espace réservé pour vis.js (Epic 3) + - [x] Catégories empilées verticalement -- [ ] **Task 7: Représentation visuelle du niveau** (AC: #2) - - [ ] Créer une barre de progression stylisée (style RPG/XP) - - [ ] Utiliser `sky-accent` pour la partie remplie - - [ ] Afficher le ratio (ex: 4/5 ou 80%) - - [ ] Animation subtile au chargement (remplissage progressif) +- [x] **Task 7: Représentation visuelle du niveau** (AC: #2) + - [x] Créer une barre de progression stylisée (style RPG/XP) + - [x] Utiliser `sky-accent` pour la partie remplie + - [x] Afficher le ratio (ex: 4/5 ou 80%) + - [x] Animation subtile au chargement (remplissage progressif) -- [ ] **Task 8: Meta tags SEO** (AC: #7) - - [ ] Titre dynamique : "Compétences | Skycel" - - [ ] Description : compétences de Célian - - [ ] og:title et og:description +- [x] **Task 8: Meta tags SEO** (AC: #7) + - [x] Titre dynamique : "Compétences | Skycel" + - [x] Description : compétences de Célian + - [x] og:title et og:description -- [ ] **Task 9: Tests et validation** - - [ ] Tester en FR et EN - - [ ] Vérifier le groupement par catégorie - - [ ] Valider les animations - - [ ] Tester le responsive - - [ ] Vérifier que les skills sont cliquables (préparation Story 2.5) +- [x] **Task 9: Tests et validation** + - [x] Tester en FR et EN + - [x] Vérifier le groupement par catégorie + - [x] Valider les animations + - [x] Tester le responsive + - [x] Vérifier que les skills sont cliquables (préparation Story 2.5) ## Dev Notes @@ -552,16 +552,46 @@ frontend/i18n/en.json # AJOUTER clés skills.* ### Agent Model Used -{{agent_model_name_version}} +Claude Opus 4.5 (claude-opus-4-5-20251101) ### Debug Log References +- API SkillController et SkillResource existaient déjà, améliorés avec level, project_count et category_label + ### Completion Notes List +- Modèle Skill amélioré : getCurrentLevel() + scope ordered par category puis display_order +- SkillResource amélioré : ajout level, display_order, project_count +- SkillController amélioré : groupement par catégorie avec category_label traduit +- Type skill.ts créé avec Skill, SkillCategory, SkillsResponse +- Composable useFetchSkills créé +- SkillCard créé : icône, nom, barre de progression animée, niveau X/Y, hover effect +- Page compétences complète : + - Grille responsive 2/3/4 colonnes + - Sections par catégorie avec icônes (🎨 frontend, ⚙️ backend, 🛠️ tools, 💡 soft_skills) + - Animation stagger fadeInUp + - États loading (skeleton), error (retry), empty + - Placeholder vis.js (desktop only) +- prefers-reduced-motion respecté +- SEO via useSeo +- Store progression visitSection('competences') +- Traductions FR/EN ajoutées (skills.*) + ### 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/Models/Skill.php` — MODIFIÉ (getCurrentLevel, scope ordered) +- `api/app/Http/Controllers/Api/SkillController.php` — MODIFIÉ (groupement, category_label) +- `api/app/Http/Resources/SkillResource.php` — MODIFIÉ (level, project_count) +- `frontend/app/types/skill.ts` — CRÉÉ +- `frontend/app/composables/useFetchSkills.ts` — CRÉÉ +- `frontend/app/components/feature/SkillCard.vue` — CRÉÉ +- `frontend/app/pages/competences.vue` — RÉÉCRIT +- `frontend/i18n/fr.json` — MODIFIÉ (ajout skills.*) +- `frontend/i18n/en.json` — MODIFIÉ (ajout skills.*) + diff --git a/docs/implementation-artifacts/sprint-status.yaml b/docs/implementation-artifacts/sprint-status.yaml index b7a5871..082308c 100644 --- a/docs/implementation-artifacts/sprint-status.yaml +++ b/docs/implementation-artifacts/sprint-status.yaml @@ -60,7 +60,7 @@ development_status: 2-1-composant-projectcard: review 2-2-page-projets-galerie: review 2-3-page-projet-detail: review - 2-4-page-competences-affichage-categories: ready-for-dev + 2-4-page-competences-affichage-categories: review 2-5-competences-cliquables-projets-lies: ready-for-dev 2-6-page-temoignages-migrations-bdd: ready-for-dev 2-7-composant-dialogue-pnj: ready-for-dev diff --git a/frontend/app/components/feature/SkillCard.vue b/frontend/app/components/feature/SkillCard.vue new file mode 100644 index 0000000..766ea84 --- /dev/null +++ b/frontend/app/components/feature/SkillCard.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/frontend/app/composables/useFetchSkills.ts b/frontend/app/composables/useFetchSkills.ts new file mode 100644 index 0000000..f3d1f90 --- /dev/null +++ b/frontend/app/composables/useFetchSkills.ts @@ -0,0 +1,14 @@ +import type { SkillsResponse } from '~/types/skill' + +export function useFetchSkills() { + const config = useRuntimeConfig() + const { locale } = useI18n() + + return useFetch('/skills', { + 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/competences.vue b/frontend/app/pages/competences.vue index 3f4caf3..3d303eb 100644 --- a/frontend/app/pages/competences.vue +++ b/frontend/app/pages/competences.vue @@ -1,16 +1,153 @@ + + diff --git a/frontend/app/types/skill.ts b/frontend/app/types/skill.ts new file mode 100644 index 0000000..a5991b1 --- /dev/null +++ b/frontend/app/types/skill.ts @@ -0,0 +1,30 @@ +export interface Skill { + id: number + slug: string + name: string + description: string | null + icon: string | null + category: string + level: number + max_level: number + display_order: number + project_count?: number + pivot?: { + level_before: number + level_after: number + } +} + +export interface SkillCategory { + category: string + category_label: string + skills: Skill[] +} + +export interface SkillsResponse { + data: SkillCategory[] + meta: { + lang: string + total: number + } +} diff --git a/frontend/i18n/en.json b/frontend/i18n/en.json index 19ef7ab..53ff1e2 100644 --- a/frontend/i18n/en.json +++ b/frontend/i18n/en.json @@ -94,6 +94,17 @@ "previous": "Previous project", "next": "Next project" }, + "skills": { + "title": "My Skills", + "page_title": "Skills | Skycel", + "page_description": "Discover the technical skills and soft skills of C\u00e9lian, full-stack web developer.", + "load_error": "Unable to load skills...", + "no_skills": "No skills yet", + "skill_tree_placeholder": "Interactive skill tree (coming soon)", + "level": "Level", + "project": "project", + "projects": "projects" + }, "pages": { "projects": { "title": "Projects", diff --git a/frontend/i18n/fr.json b/frontend/i18n/fr.json index c99f3b6..c135e9d 100644 --- a/frontend/i18n/fr.json +++ b/frontend/i18n/fr.json @@ -94,14 +94,25 @@ "previous": "Projet pr\u00e9c\u00e9dent", "next": "Projet suivant" }, + "skills": { + "title": "Mes Comp\u00e9tences", + "page_title": "Comp\u00e9tences | Skycel", + "page_description": "D\u00e9couvrez les comp\u00e9tences techniques et soft skills de C\u00e9lian, d\u00e9veloppeur web full-stack.", + "load_error": "Impossible de charger les comp\u00e9tences...", + "no_skills": "Aucune comp\u00e9tence pour le moment", + "skill_tree_placeholder": "Arbre de comp\u00e9tences interactif (bient\u00f4t disponible)", + "level": "Niveau", + "project": "projet", + "projects": "projets" + }, "pages": { "projects": { "title": "Projets", - "description": "Découvrez mes projets et réalisations" + "description": "D\u00e9couvrez mes projets et r\u00e9alisations" }, "skills": { - "title": "Compétences", - "description": "Mes compétences techniques et humaines" + "title": "Comp\u00e9tences", + "description": "Mes comp\u00e9tences techniques et humaines" }, "testimonials": { "title": "Témoignages",