# Story 2.5: Compétences cliquables → Projets liés Status: ready-for-dev ## Story As a visiteur, I want cliquer sur une compétence pour voir les projets qui l'utilisent, so that je peux voir des preuves concrètes de maîtrise. ## Acceptance Criteria 1. **Given** le visiteur est sur la page Compétences **When** il clique sur une compétence **Then** un panneau/modal s'ouvre avec la liste des projets liés à cette compétence 2. **And** pour chaque projet lié : titre, description courte, lien vers le détail 3. **And** l'indication du niveau avant/après chaque projet est visible (progression) 4. **And** une animation d'ouverture/fermeture fluide est présente (respectant `prefers-reduced-motion`) 5. **And** la fermeture est possible par clic extérieur, bouton close, ou touche Escape 6. **And** le panneau/modal utilise Headless UI pour l'accessibilité 7. **And** la navigation au clavier est fonctionnelle (Tab, Escape, Enter) 8. **And** le focus est piégé dans le modal quand ouvert (`focus trap`) 9. **And** les données viennent de la relation `skill_project` via l'API ## Tasks / Subtasks - [ ] **Task 1: Créer l'endpoint API pour les projets d'une compétence** (AC: #9) - [ ] Ajouter méthode `projects($slug)` dans `SkillController` - [ ] Charger les projets avec leur pivot (level_before, level_after) - [ ] Retourner 404 si le skill n'existe pas - [ ] Joindre les traductions - [ ] **Task 2: Installer et configurer Headless UI** (AC: #6) - [ ] Installer `@headlessui/vue` dans le frontend - [ ] Vérifier la compatibilité avec Vue 3 / Nuxt 4 - [ ] **Task 3: Créer le composant SkillProjectsModal** (AC: #1, #2, #3, #5, #6, #7, #8) - [ ] Créer `frontend/app/components/feature/SkillProjectsModal.vue` - [ ] Utiliser `Dialog` de Headless UI - [ ] Props : isOpen, skill (avec name, description) - [ ] Emit : close - [ ] Afficher le titre de la compétence - [ ] Afficher la description de la compétence - [ ] Liste des projets liés - [ ] **Task 4: Créer le composant ProjectListItem** (AC: #2, #3) - [ ] Créer `frontend/app/components/feature/ProjectListItem.vue` - [ ] Afficher titre, description courte, niveau avant/après - [ ] Lien vers la page détail du projet - [ ] Visualisation de la progression (flèche niveau) - [ ] **Task 5: Charger les projets au clic** (AC: #9) - [ ] Créer composable `useFetchSkillProjects(slug)` - [ ] Appeler l'API quand le modal s'ouvre - [ ] Gérer l'état loading/error dans le modal - [ ] **Task 6: Implémenter les animations** (AC: #4) - [ ] Animation d'ouverture : fade-in + scale - [ ] Animation de fermeture : fade-out + scale - [ ] Overlay avec backdrop blur - [ ] Respecter `prefers-reduced-motion` - [ ] **Task 7: Fermeture du modal** (AC: #5) - [ ] Clic sur l'overlay ferme le modal - [ ] Bouton close (X) en haut à droite - [ ] Touche Escape ferme le modal - [ ] Restaurer le focus à l'élément précédent - [ ] **Task 8: Intégrer dans la page Compétences** (AC: #1) - [ ] Modifier `competences.vue` pour ouvrir le modal - [ ] Gérer l'état du modal (isOpen, selectedSkill) - [ ] Passer les props au modal - [ ] **Task 9: Tests et validation** - [ ] Tester l'ouverture/fermeture - [ ] Valider la navigation clavier (Tab, Escape) - [ ] Tester le focus trap - [ ] Vérifier l'accessibilité avec axe DevTools - [ ] Tester en FR et EN - [ ] Valider les animations ## Dev Notes ### Endpoint API Laravel ```php header('Accept-Language', 'fr'); $skill = Skill::with('projects') ->where('slug', $slug) ->first(); if (!$skill) { return response()->json([ 'error' => [ 'code' => 'SKILL_NOT_FOUND', 'message' => 'Skill not found', ] ], 404); } return response()->json([ 'data' => [ 'skill' => [ 'id' => $skill->id, 'slug' => $skill->slug, 'name' => Translation::getTranslation($skill->name_key, $lang), 'description' => Translation::getTranslation($skill->description_key, $lang), 'level' => $skill->getCurrentLevel(), 'maxLevel' => $skill->max_level, ], 'projects' => $skill->projects->map(function ($project) use ($lang) { return [ 'id' => $project->id, 'slug' => $project->slug, 'title' => Translation::getTranslation($project->title_key, $lang), 'shortDescription' => Translation::getTranslation($project->short_description_key, $lang), 'image' => $project->image, 'dateCompleted' => $project->date_completed?->format('Y-m-d'), 'levelBefore' => $project->pivot->level_before, 'levelAfter' => $project->pivot->level_after, 'levelDescription' => $project->pivot->level_description_key ? Translation::getTranslation($project->pivot->level_description_key, $lang) : null, ]; }), ], 'meta' => ['lang' => $lang], ]); } ``` ```php // api/routes/api.php Route::get('/skills/{slug}/projects', [SkillController::class, 'projects']); ``` ### Installation Headless UI ```bash cd frontend npm install @headlessui/vue ``` ### Composable useFetchSkillProjects ```typescript // frontend/app/composables/useFetchSkillProjects.ts import type { Skill } from '~/types/skill' import type { Project } from '~/types/project' interface SkillProjectsResponse { data: { skill: Skill projects: (Project & { levelBefore: number; levelAfter: number; levelDescription?: string })[] } meta: { lang: string } } export function useFetchSkillProjects(slug: Ref) { const config = useRuntimeConfig() const { locale } = useI18n() return useFetch( () => slug.value ? `/skills/${slug.value}/projects` : null, { baseURL: config.public.apiUrl, headers: { 'X-API-Key': config.public.apiKey, 'Accept-Language': locale.value, }, immediate: false, watch: false, } ) } ``` ### Composant SkillProjectsModal ```vue ``` ### Composant ProjectListItem ```vue ``` ### Modification de competences.vue ```vue ``` ### Clés i18n nécessaires **fr.json :** ```json { "skills": { "relatedProjects": "Projets utilisant cette compétence", "loadProjectsError": "Impossible de charger les projets liés", "noProjects": "Aucun projet n'utilise encore cette compétence", "level": "Niveau" }, "common": { "close": "Fermer" } } ``` **en.json :** ```json { "skills": { "relatedProjects": "Projects using this skill", "loadProjectsError": "Unable to load related projects", "noProjects": "No projects use this skill yet", "level": "Level" }, "common": { "close": "Close" } } ``` ### Accessibilité | Requirement | Implementation | |-------------|----------------| | Focus trap | Géré automatiquement par Headless UI Dialog | | Keyboard navigation | Tab entre les éléments, Escape pour fermer | | Screen reader | DialogTitle annoncé, aria-modal="true" | | Fermeture externe | Clic overlay, bouton X, Escape | | Focus restoration | Automatique par Headless UI | ### Dépendances **Cette story nécessite :** - Story 1.2 : Table skill_project avec relations - Story 2.4 : Page Compétences avec SkillCard cliquable **Cette story prépare pour :** - Aucune dépendance directe ### Project Structure Notes **Fichiers à créer :** ``` frontend/app/components/feature/ ├── SkillProjectsModal.vue # CRÉER └── ProjectListItem.vue # CRÉER frontend/app/composables/ └── useFetchSkillProjects.ts # CRÉER ``` **Fichiers à modifier :** ``` api/app/Http/Controllers/Api/SkillController.php # AJOUTER projects() api/routes/api.php # AJOUTER route frontend/app/pages/competences.vue # AJOUTER modal frontend/i18n/fr.json # AJOUTER clés frontend/i18n/en.json # AJOUTER clés frontend/package.json # AJOUTER @headlessui/vue ``` ### References - [Source: docs/planning-artifacts/epics.md#Story-2.5] - [Source: docs/planning-artifacts/architecture.md#Design-System-Components] - [Source: docs/planning-artifacts/ux-design-specification.md#Design-System-Components-Headless] - [Source: docs/planning-artifacts/ux-design-specification.md#Accessibility-Strategy] ### Technical Requirements | Requirement | Value | Source | |-------------|-------|--------| | UI Library | Headless UI Dialog | Architecture | | Focus trap | Required | WCAG AA | | Keyboard nav | Tab, Escape, Enter | WCAG AA | | Animation | Respect prefers-reduced-motion | NFR6 | | API endpoint | GET /api/skills/{slug}/projects | Architecture | ## Dev Agent Record ### Agent Model Used {{agent_model_name_version}} ### Debug Log References ### Completion Notes List ### Change Log | Date | Change | Author | |------|--------|--------| | 2026-02-04 | Story créée avec contexte complet | SM Agent | ### File List