# Story 2.4: Page Compétences - Affichage par catégories Status: review ## Story As a visiteur, I want voir les compétences du développeur organisées par catégorie, so that je comprends son profil technique global. ## Acceptance Criteria 1. **Given** le visiteur accède à `/competences` (FR) ou `/en/skills` (EN) **When** la page se charge **Then** les compétences sont affichées groupées par catégorie (Frontend, Backend, Tools, Soft skills) 2. **And** chaque compétence affiche : icône, nom traduit, niveau actuel (représentation visuelle) 3. **And** les données sont chargées depuis l'API `/api/skills` avec le contenu traduit 4. **And** une animation d'entrée des éléments est présente (respectant `prefers-reduced-motion`) 5. **And** sur desktop : préparé pour accueillir le skill tree vis.js (Epic 3) 6. **And** sur mobile : liste groupée par catégorie avec design adapté 7. **And** les meta tags SEO sont dynamiques pour cette page 8. **And** chaque compétence est visuellement cliquable (affordance) ## Tasks / Subtasks - [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` - [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[] - [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) - [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 - [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` - [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 - [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) - [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 - [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 ### Endpoint API Laravel ```php header('Accept-Language', 'fr'); $skills = Skill::with('projects') ->orderBy('category') ->orderBy('display_order') ->get(); // Grouper par catégorie $grouped = $skills->groupBy('category'); return response()->json([ 'data' => $grouped->map(function ($categorySkills, $category) use ($lang) { return [ 'category' => $category, 'categoryLabel' => $this->getCategoryLabel($category, $lang), 'skills' => SkillResource::collection($categorySkills), ]; })->values(), '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] ?? $category; } } ``` ```php header('Accept-Language', 'fr'); return [ 'id' => $this->id, 'slug' => $this->slug, 'name' => Translation::getTranslation($this->name_key, $lang), 'description' => Translation::getTranslation($this->description_key, $lang), 'icon' => $this->icon, 'category' => $this->category, 'level' => $this->getCurrentLevel(), 'maxLevel' => $this->max_level, 'displayOrder' => $this->display_order, 'projectCount' => $this->whenLoaded('projects', fn () => $this->projects->count()), ]; } } ``` ```php // api/app/Models/Skill.php - Ajouter méthode public function getCurrentLevel(): int { // Retourne le niveau max atteint dans tous les projets $maxLevelAfter = $this->projects() ->max('skill_project.level_after'); return $maxLevelAfter ?? 1; } ``` ```php // api/routes/api.php Route::get('/skills', [SkillController::class, 'index']); ``` ### Composable useFetchSkills ```typescript // frontend/app/composables/useFetchSkills.ts import type { Skill, SkillCategory } from '~/types/skill' interface SkillsResponse { data: SkillCategory[] meta: { lang: string; total: number } } export function useFetchSkills() { const config = useRuntimeConfig() const { locale } = useI18n() return useFetch('/skills', { baseURL: config.public.apiUrl, headers: { 'X-API-Key': config.public.apiKey, 'Accept-Language': locale.value, }, }) } ``` ### Types TypeScript ```typescript // frontend/app/types/skill.ts export interface Skill { id: number slug: string name: string description: string icon: string | null category: string level: number maxLevel: number displayOrder: number projectCount?: number } export interface SkillCategory { category: string categoryLabel: string skills: Skill[] } ``` ### Composant SkillCard ```vue ``` ### Page competences.vue ```vue ``` ### Clés i18n nécessaires **fr.json :** ```json { "skills": { "title": "Mes Compétences", "pageTitle": "Compétences | Skycel", "pageDescription": "Découvrez les compétences techniques et soft skills de Célian, développeur web full-stack.", "loadError": "Impossible de charger les compétences...", "skillTreePlaceholder": "Arbre de compétences interactif (bientôt disponible)", "level": "Niveau", "projects": "projets" } } ``` **en.json :** ```json { "skills": { "title": "My Skills", "pageTitle": "Skills | Skycel", "pageDescription": "Discover the technical skills and soft skills of Célian, full-stack web developer.", "loadError": "Unable to load skills...", "skillTreePlaceholder": "Interactive skill tree (coming soon)", "level": "Level", "projects": "projects" } } ``` ### Catégories de compétences | Catégorie | Couleur de zone (Future) | Exemples | |-----------|-------------------------|----------| | Frontend | Teinte bleue | Vue.js, Nuxt, TypeScript, TailwindCSS | | Backend | Teinte verte | Laravel, PHP, Node.js, MySQL | | Tools | Teinte jaune | Git, Docker, VS Code | | Soft Skills | Teinte violette | Communication, Gestion de projet | ### Dépendances **Cette story nécessite :** - Story 1.2 : Table skills, Model Skill avec relations - Story 1.3 : Système i18n configuré **Cette story prépare pour :** - Story 2.5 : Compétences cliquables → Projets liés (le clic est déjà émis) - Story 3.5 : Skill tree vis.js (Epic 3) - placeholder préparé ### Project Structure Notes **Fichiers à créer :** ``` api/app/Http/ ├── Controllers/Api/ │ └── SkillController.php # CRÉER └── Resources/ └── SkillResource.php # CRÉER frontend/app/ ├── pages/ │ └── competences.vue # CRÉER ├── components/ │ └── feature/ │ └── SkillCard.vue # CRÉER ├── composables/ │ └── useFetchSkills.ts # CRÉER └── types/ └── skill.ts # CRÉER ``` **Fichiers à modifier :** ``` api/app/Models/Skill.php # AJOUTER getCurrentLevel() api/routes/api.php # AJOUTER route /skills frontend/i18n/fr.json # AJOUTER clés skills.* frontend/i18n/en.json # AJOUTER clés skills.* ``` ### References - [Source: docs/planning-artifacts/epics.md#Story-2.4] - [Source: docs/planning-artifacts/architecture.md#API-&-Communication-Patterns] - [Source: docs/planning-artifacts/ux-design-specification.md#Component-Strategy] - [Source: docs/planning-artifacts/ux-design-specification.md#SkillTree] ### Technical Requirements | Requirement | Value | Source | |-------------|-------|--------| | API endpoint | GET /api/skills | Architecture | | Groupement | Par catégorie | Epics | | Niveau visuel | Barre de progression | Epics | | Placeholder vis.js | Desktop only | Epics | | Animation | Stagger + respect motion | NFR6 | ## Dev Agent Record ### Agent Model Used 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.*)